Filter concepts by levelShowing all levels.

System Design · Section 73

Dependency Management

Level
intermediate
Read
12 min
Concepts
2

Dependency management starts with an explicit, maintained inventory of every downstream call a service makes — internal services, databases, queues, third-party APIs — written down rather than held as tribal knowledge, because an on-call engineer needs to answer "what breaks if X goes down" without reconstructing the architecture from code during an incident. Each edge in that map then gets a deliberate classification: critical (the primary function cannot complete without it, and no reasonable fallback exists) or optional (failure should degrade the experience, not eliminate it) — a distinction the code itself does not make, since a blocking call to a critical dependency and a blocking call to an optional one look identical in source. That classification is what determines which of five standard defensive mechanisms to apply: timeouts, bulkheads and circuit breakers contain a critical dependency's failure and let it fail fast and visibly, while fallback behavior and asynchronous decoupling hide an optional dependency's failure from the caller's success path entirely. The mechanisms themselves are each covered in full elsewhere in this topic (Circuit Breakers and Bulkheads, Timeouts and Resource Limits, Retry Strategy) — this section's actual contribution is the discipline of matching the right one to each dependency based on its classification, rather than defaulting every downstream call to the same treatment.

System Design overview

What is true here

  1. A dependency map is a written, maintained inventory of every downstream call — not reconstructed from memory or code during an incident.
  2. Critical vs optional is a deliberate classification (can the primary function complete without it, and is there a reasonable fallback), not something the code's syntax decides for you.
  3. Critical dependencies get timeout + bulkhead + circuit breaker — contain the blast radius, fail fast and visibly.
  4. Optional dependencies get fallback behavior or asynchronous decoupling — hide the failure from the caller's success path entirely.
  5. A shared resource pool across dependencies couples all of them to whichever one is currently slowest — a bulkhead exists to break that coupling.

What you will be able to do

  • Build and maintain a dependency map for a service, rather than relying on tribal knowledge
  • Classify a given dependency as critical or optional using a concrete test, not intuition
  • Choose the correct defensive mechanism (timeout/bulkhead/circuit breaker vs fallback/async decoupling) for a given classification
  • Recognize when a fallback is hiding a failure that should have stopped the request, rather than genuinely degrading gracefully

Mapping and classifying

Writing down every downstream dependency and deciding, deliberately, which ones are critical.

Building a dependency map: critical vs optional

coreintermediate

A dependency map is an explicit, drawn-out record of every other service, database, queue and third-party API a service calls, in one place — most teams have this knowledge scattered across individual engineers' heads rather than written down, which means nobody can answer "what breaks if the recommendations service goes down" without guessing. Once the map exists, the next step is classifying each edge as critical or optional: a critical dependency is one whose failure means the calling service cannot do its primary job at all (a checkout service cannot function without the payment gateway), while an optional dependency is one whose failure should degrade the experience but not break it (a product page can render without the recommendations service, just without the "customers also bought" section). This distinction is not obvious from the code alone — a synchronous, blocking call to the recommendations service looks identical in the code to a synchronous, blocking call to the payment gateway, even though one failure is catastrophic and the other should be invisible to the end user. Making the classification explicit is what tells an engineer which calls need a fallback and which calls genuinely justify blocking the whole request.

Think of it as

A dependency map is like a building's utility diagram: it shows exactly which pipes and wires actually feed which room, drawn out once by an electrician rather than reconstructed from memory during an outage. Critical vs optional is the difference between the wire that powers the emergency lighting (the building genuinely cannot be occupied without it — critical) and the wire that powers the decorative fountain in the lobby (nice to have, and the building operates completely fine without it — optional). A fire inspector does not need to guess which is which during an actual fire; the diagram already says so, and a well-run building maintains it as circuits change, not just draws it once at construction and forgets it.

text
# A minimal dependency map entry, one per service:
service: checkout-service
  critical:
    - payment-gateway (cannot complete a purchase without it)
    - inventory-service (cannot confirm stock without it)
    - primary database (cannot record the order without it)
  optional:
    - recommendations-service (upsell suggestions; hide on failure)
    - loyalty-points-service (award points async; retry later on failure)
    - analytics-pipeline (fire-and-forget event; never blocks checkout)

What we're doing: Classify five downstream calls a checkout service makes.

checkout-dependencies.txttext
checkout-service calls:
1. payment-gateway.charge()
2. inventory-service.reserve_stock()
3. recommendations-service.get_upsells()
4. loyalty-points-service.award_points()
5. analytics.track_purchase()
2
Critical: without a successful charge, there is no purchase — there is no reasonable fallback that lets checkout "succeed" without payment actually happening.
3
Critical: if stock cannot be confirmed and reserved, the order should not be accepted — allowing it anyway risks selling inventory that does not exist.
4
Optional: an upsell suggestion is a nice-to-have on the confirmation page; if the call fails or times out, the page renders without it and the purchase proceeds normally.
5
Optional: points can be awarded on a short delay via a retry queue if the loyalty service is briefly down — no user-visible impact to checkout itself.
6
Optional, and further, should not even be a blocking call at all — an analytics event is fire-and-forget, and a checkout that fails because the analytics pipeline was slow is a design bug, not an acceptable trade-off.

Why this works: The five calls look interchangeable in code — five function calls inside the same request handler — but only two of them are actually critical; treating all five as equally blocking (the default when nobody has done this classification) means an analytics outage can take down checkout, which is a self-inflicted failure with no corresponding benefit.

Writing every downstream call as synchronous and blocking by default

Wrong

python
def checkout(order):
    payment_gateway.charge(order)
    inventory_service.reserve_stock(order)
    recommendations_service.get_upsells(order)   # blocks checkout
    loyalty_points_service.award_points(order)   # blocks checkout
    analytics.track_purchase(order)              # blocks checkout
    return confirm(order)

Better

python
def checkout(order):
    payment_gateway.charge(order)          # critical: let it block
    inventory_service.reserve_stock(order) # critical: let it block
    try:
        upsells = recommendations_service.get_upsells(order)
    except (Timeout, ServiceError):
        upsells = []                       # optional: fall back
    queue.enqueue('award_points', order)   # optional: async
    queue.enqueue('track_purchase', order) # optional: async
    return confirm(order, upsells)

What you see: A brief outage in the recommendations service, which no one classified as critical, causes every checkout attempt to time out and fail, and the incident review discovers the outage never should have been able to touch checkout at all.

Why: Writing a downstream call is the same one line of code whether the dependency is critical or optional — nothing in the syntax forces a developer to decide, so without an explicit classification step, every new integration defaults to synchronous-and-blocking simply because that is the easiest way to write it, regardless of whether the dependency actually deserves that level of coupling.

Plotting a service's dependencies
Payment gateway
checkout cannot complete without it
Inventory service
cannot confirm stock
Recommendations
hide the section on failure
Loyalty points
award asynchronously, retry later
Analytics pipeline
fire-and-forget, never blocks the request
  • Payment gateway: Critical, High blast radius — checkout cannot complete without it
  • Inventory service: Critical, High blast radius — cannot confirm stock
  • Recommendations: Optional, Low blast radius — hide the section on failure
  • Loyalty points: Optional, between Low blast radius and High blast radius — award asynchronously, retry later
  • Analytics pipeline: Optional, Low blast radius — fire-and-forget, never blocks the request

Classifying a dependency: the test that decides critical vs optional

Classifying a dependency: the test that decides critical vs optional
QuestionIf yes → criticalIf no → optional
Can the primary function complete at all without a response from this dependency?No — criticalYes — optional
Is there a reasonable fallback (cache, default value, hide the feature)?None available — criticalA fallback exists — optional
Would a user notice the feature is simply absent, versus the whole page/flow failing?Whole flow fails — criticalFeature quietly absent — optional

Remember: A dependency map is an explicit, maintained inventory of every downstream call a service makes; classifying each one as critical (failure breaks the primary function, no reasonable fallback) or optional (failure degrades gracefully) is a deliberate design decision that the code itself does not make for you — an unclassified dependency defaults to synchronous and blocking, which quietly turns every dependency into a critical one whether it deserves that or not.

See also: preventing cascading failures via decoupling · the failure mode question checklist · why circuit breakers exist

Advertisement

Matching defense to classification

The five standard mechanisms, and which ones belong on a critical dependency versus an optional one.

Preventing cascading failures at the dependency edge

coreintermediate

Once a dependency map exists and every edge is classified as critical or optional (the prior concept), the actual work of dependency management is choosing the right defensive mechanism for each edge, matched to whether it is critical or optional. Five mechanisms cover almost every case, each detailed on its own elsewhere in this topic: timeouts bound how long a call is allowed to wait, so a slow dependency cannot hold a caller's thread or connection open indefinitely; bulkheads isolate the resources (thread pools, connection pools) used to call one dependency from the resources used to call another, so one dependency's slowness cannot starve calls to a completely unrelated dependency; circuit breakers stop calling a dependency that is already failing, so a caller does not keep paying the cost (and adding load) of calls that are very likely to fail anyway; fallback behavior gives a caller something reasonable to do when a dependency fails — a cached value, a default, or simply omitting an optional feature — rather than failing the whole request; and asynchronous decoupling (a queue between caller and dependency) removes the synchronous coupling entirely for work that does not need an immediate answer, so the caller's success no longer depends on the dependency being available at that exact moment. The skill this section is actually teaching is not any one mechanism (they are each covered in full elsewhere) but matching mechanism to dependency: a critical dependency typically gets a timeout plus a bulkhead plus a circuit breaker (contain the failure, do not try to hide that it happened), while an optional dependency typically gets a fallback or gets moved behind a queue entirely (hide the failure from the caller's success path).

Think of it as

Think of a ship with watertight compartments (bulkheads), lifeboats (fallback behavior), a captain who stops sending crew into a flooding compartment once it is clearly lost (a circuit breaker), a rule that no one waits indefinitely at a flooding doorway (a timeout), and cargo that is simply not carried in a way that requires it to arrive on this exact voyage — it can go on the next ship instead (asynchronous decoupling). None of these mechanisms stops the leak itself; they stop one compartment's flood from sinking the whole ship. The dependency-management skill is knowing which compartments genuinely need a bulkhead (the engine room) and which pieces of cargo can simply go on the next ship instead of being on this one's critical path at all.

text
# Mechanism selection for the checkout example
# from the prior concept:
payment-gateway (critical):
  timeout: 3s, bulkhead: dedicated pool (20 conns),
  circuit breaker: open after 50% errors/10s
inventory-service (critical):
  timeout: 2s, bulkhead: dedicated pool (20 conns),
  circuit breaker: open after 50% errors/10s
recommendations-service (optional):
  timeout: 500ms, fallback: return empty list
loyalty-points-service (optional):
  asynchronous: enqueue, worker retries independently
analytics-pipeline (optional):
  asynchronous: fire-and-forget enqueue, never blocks

What we're doing: Trace what happens to a checkout request when the recommendations service and the payment gateway each fail, given the mechanisms assigned above.

checkout-with-defenses.pypython
def checkout(order):
    try:
        payment_gateway.charge(order)      # critical: timeout+breaker
    except (Timeout, CircuitOpenError):
        raise CheckoutFailed("payment unavailable")

    inventory_service.reserve_stock(order) # critical: same defenses

    try:
        upsells = recommendations_service.get_upsells(order)
    except (Timeout, ServiceError):
        upsells = []                        # optional: fallback

    queue.enqueue('award_points', order)    # optional: async
    return confirm(order, upsells)
4
When the payment gateway fails, the request fails loudly and immediately — this is correct: checkout genuinely cannot proceed without it, and a fast, visible failure is far better than a hung request or, worse, a "successful" checkout that never actually charged the customer.
10
When recommendations fails, the request proceeds with an empty upsell list — the failure is fully absorbed at this one line and never reaches the caller of `checkout()` at all.

Why this works: The same failure mode (a downstream service erroring or timing out) produces two completely different, both correct, outcomes depending on which mechanism was assigned — this is the entire point of doing the classification work in the prior concept before choosing a mechanism, rather than reaching for the same default (usually: none, or an unconditional retry) everywhere.

Wrapping a critical dependency in a fallback that hides a failure that should stop the request

Wrong

python
def checkout(order):
    try:
        payment_gateway.charge(order)
    except (Timeout, ServiceError):
        pass  # "fallback": proceed anyway
    inventory_service.reserve_stock(order)
    return confirm(order)

Better

python
def checkout(order):
    try:
        payment_gateway.charge(order)
    except (Timeout, CircuitOpenError):
        raise CheckoutFailed("payment unavailable")
    inventory_service.reserve_stock(order)
    return confirm(order)

What you see: Orders are confirmed and shipped without ever having actually been charged, discovered only when finance reconciles revenue against fulfilled orders weeks later, because a well-intentioned "fallback" swallowed a payment failure instead of stopping the checkout.

Why: Fallback behavior is the right mechanism for an optional dependency precisely because proceeding without it is a genuinely acceptable outcome — applying the same pattern to a critical dependency does not make the failure acceptable, it just hides an unacceptable outcome (an unpaid order) behind code that looks like a resilience improvement.

Five mechanisms, matched to two dependency types

Critical dependency defenses

Timeout

bound the wait

Bulkhead

isolate the resource pool

Circuit breaker

stop calling a failing dependency

Optional dependency defenses

Fallback

cache, default, or omit the feature

Async decoupling

queue removes the synchronous coupling

  • Critical dependency defenses
    • Timeout — bound the wait
    • Bulkhead — isolate the resource pool
    • Circuit breaker — stop calling a failing dependency
  • Optional dependency defenses
    • Fallback — cache, default, or omit the feature
    • Async decoupling — queue removes the synchronous coupling

Matching mechanism to dependency classification

Matching mechanism to dependency classification
Dependency typeTypical mechanismsGoal
CriticalTimeout + bulkhead + circuit breakerContain the blast radius; fail visibly and fast rather than hang
OptionalFallback behavior, or asynchronous decouplingMake the failure invisible to the caller's success path

Remember: Five mechanisms — timeouts, bulkheads, circuit breakers, fallback behavior, asynchronous decoupling — cover almost every dependency-protection need; the skill is matching mechanism to classification, not applying one everywhere. Critical dependencies get contained and fail visibly (timeout + bulkhead + circuit breaker); optional dependencies get hidden from the caller's success path entirely (fallback or async decoupling). Wrapping a critical dependency in a fallback that silently proceeds is usually worse than no protection at all.

See also: mapping critical vs optional dependencies · why circuit breakers exist · bulkhead isolation · connect read request deadlines · backoff and jitter

Advertisement