Session 6 · Week 3 · Thu 19 Nov 2026 · 75 min

Designing for Failure.

Blast radius, fail-open vs fail-closed, and the resilience patterns that keep a failure local. Two live drills, four real outages.

Ehsan Gazar
From Senior to Staff · session 6 of 8
02:14 · a true-to-life incident

Recommendations got 300ms slower. Nine minutes later, checkout was down.

02:14Recommendations API +300msa blip, nothing pages
02:17Shared thread pool saturatesevery request waits behind it
02:19Clients time out, retry 3× in lockstepthe storm forms
02:23Checkout and login start failingunrelated paths, same pool
02:31Full outage, long after recs recoveredthe fault healed, the site didn't
🔍 Hold this question

Recommendations was never business-critical. So why is checkout down? By the last slide you'll have a firebreak for every line above, and a name for each one.

Where we're going · 90 minutes

The run sheet.

0–10Everything failsthe 2:14am cold open
10–30Blast radiuswhat takes down what + map yours (drill)
30–55Resilience patternstimeout, retry+jitter, breaker, bulkhead
55–75Fail-open vs fail-closedthe highest-signal sentence, said out loud (drill)
75–90Degrade + spot the sinspartial service, and a review drill
By the end of tonight

You'll be able to…

1Compute the blast radius of a component before it fails.
2Choose fail-open vs fail-closed deliberately, per path.
3Apply timeouts, retries, circuit breakers, and bulkheads.
4Design graceful degradation instead of all-or-nothing.
The reframe

You don't prevent failure. You contain it.

At scale something is always broken. Reliability isn't the absence of failure, it's making sure a failure stays small and visible. The design question is never "will it fail" but "what does it take down with it?"

💡 In plain English

You can't stop a fuse from ever blowing. So you wire the house so one blown fuse darkens one room, not the whole building, and you make sure a light comes on to tell you which one went.

Blast radius
name what each failure takes with it
first move
Fail-open or closed?
decide per path and say why
high signal
Degrade, don't die
serve something useful when a dependency is down
goal
Blast radius · 10–30 min

Blast radius: what one failure takes down with it.

Before a component ships, you should be able to say exactly what dies when it dies. Walk the dependency graph outward from the component: every caller that can't tolerate its absence is inside the blast radius. You compute this before the incident, not during.

flowchart TB
  AUTH(["Auth service"]) --> API(["API gateway"])
  CACHE(["Session cache"]) --> API
  API --> WEB(["Web app"])
  API --> MOB(["Mobile app"])
  REC(["Recommendations"]) --> WEB
  classDef hit fill:#FEE4E2,stroke:#1F2937,color:#0E1726;
  classDef ok fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
  classDef opt fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
  class AUTH,API,WEB,MOB hit;
  class CACHE ok;
  class REC opt;
        
🔥 It really happened — AWS S3, 2017

A command typo took S3 offline in us-east-1 for ~4 hours and much of the web went with it. The kicker: the AWS status dashboard itself depended on S3, so it stayed green through the whole outage. Nobody had mapped their own blast radius.

Blast radius · the method

Compute it: critical vs optional, per edge.

For every dependency edge, label it hard (caller cannot function without it) or soft (caller degrades but survives). The blast radius of a node is everything reachable through hard edges. The cheapest reliability win in any review is turning a hard edge into a soft one.

1List edgeswho calls whom, both directions
2Label hard / softcan the caller survive its absence?
3Trace hard edgesthat set is the blast radius
4Soften the worstturn one hard edge soft
💡 In plain English

"Hard" is a load-bearing wall, knock it out and the floor above falls. "Soft" is a decorative panel, lose it and the room just looks worse. Reliability work is mostly converting walls into panels.

Drill · do it now · 4 min

Map the blast radius of your capstone.

⏱ 4 minutes — then two of you read yours out
1Sketch your top 5 dependencies as boxes and arrows, the direction of the call.DB, auth, a queue, a third-party API, a cache — whatever your capstone leans on.
2Label every edge hard or soft.The test is one question: can the caller still serve a useful response without it?
3Trace the hard edges and circle the set.That circle is your blast radius. Be honest about how big it is.
4Pick the one hard edge you'd soften first.That single sentence is the opening of your Project 4 · ADR #3.
💡 Why we stop here

You learn resilience by drawing your own graph, not by watching mine. Four minutes now saves an hour of the capstone later.

Resilience patterns · 30–55 min

Bound every call. Four patterns do it.

An unbounded call is how a single slow dependency becomes a site-wide outage. These four patterns each bound a different axis: time, repetition, persistence, and concurrency. We take one slide each, with the failure it prevents.

1
Timeout
bounds time · no call is unbounded
2
Retry + jitter
bounds repetition · retry the retryable, with backoff
3
Circuit breaker
bounds persistence · stop hammering the dead
4
Bulkhead
bounds concurrency · isolate the pools
Pattern 1 · Timeout · bounds time

A hang is the worst failure. Bound every hop.

A crash is honest, it frees the thread and surfaces an error. A hang holds the thread, the connection, and the user, forever. Give every outbound call a timeout, and make the timeouts add up: each hop gets a slice of the budget left by the hop above it.

flowchart LR
  U(["User
budget 1000ms"]) --> A(["API
900ms left"]) A --> B(["Service
600ms left"]) B --> D(["DB query
400ms cap"]) D -.->|"over budget?"| X(["fail fast,
don't hang the chain"]) classDef io fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef ok fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; class U,A,B io; class D ok; class X bad;
💡 In plain English

If you've got 10 seconds to catch a train, you don't give the ticket machine 9 and the platform run 9. You split the 10 across every step, and bail the moment a step blows its share. Timeout budgets are that split.

Pattern 2 · Retry + jitter · bounds repetition

Retry only the retryable. Add backoff and jitter.

Retries paper over a blip, but a naïve retry is a loaded gun. Retry only idempotent, transient failures (timeouts, 503s), never a 400. Back off exponentially, and add jitter so a thousand clients don't all retry on the same beat and hammer a recovering service flat again.

flowchart TB
  F(["dependency blips"]) --> S{"retry strategy?"}
  S -- "immediate, in lockstep" --> STORM(["retry storm:
synchronized spike
re-kills the service"]) S -- "backoff + jitter" --> CALM(["spread-out retries:
service recovers"]) classDef io fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef ask fill:#ffffff,stroke:#1F2937,color:#0E1726; classDef good fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; class F io; class S ask; class CALM good; class STORM bad;
🔥 It really happened — AWS DynamoDB, 2015

An overloaded metadata service triggered a retry storm that kept re-killing it for hours across us-east-1. AWS's fix, and the write-up engineers still cite today, was exponential backoff plus jitter.

Retry math · the number that scares people

Retries multiply down the stack. Then they explode.

Retries feel local, but they compound at every hop. If each of four layers retries three times on failure, one user click doesn't make one backend call. It makes…

3⁴ = 81 81 calls from a single click, all aimed at the dependency that's already falling over. Your retry policy just wrote a denial-of-service against yourself. Jitter spreads them, a breaker stops them, and a shared retry budget caps the total.
💡 In plain English

Four people each ask three times when they don't hear back. The person at the bottom of the chain gets asked eighty-one times. They were only slow, now they're buried.

Pattern 3 · Circuit breaker · bounds persistence

Stop calling the dead. Closed → open → half-open.

When a dependency is clearly down, retrying just wastes threads and slows everyone. A breaker watches the failure rate: trip open and fail fast for a cool-off, then go half-open to let one trial call test the water before you trust it again. Without half-open you either stay dark forever or slam it back open blind.

stateDiagram-v2
  [*] --> Closed
  Closed --> Open: failure rate over threshold
  Open --> HalfOpen: cool-off elapsed
  HalfOpen --> Closed: trial call succeeds
  HalfOpen --> Open: trial call fails
  note right of Closed: calls flow, failures counted
  note right of Open: fail fast, no calls
        
🔥 Why Netflix built Hystrix

At Netflix scale, one struggling downstream tied up threads across dozens of services until the whole edge browned out. Their answer, the Hystrix circuit breaker, made "stop calling the dead, then probe gently" an industry default.

Pattern 4 · Bulkhead · bounds concurrency

Isolate the pools so one leak can't sink the ship.

Share one thread or connection pool across every dependency and the slowest one starves all the others: a stuck "recommendations" call holds threads that "checkout" needed. Give each dependency its own bounded pool. One floods, the rest sail on.

flowchart TB
  subgraph Shared["One shared pool · fragile"]
    R1(["slow dep eats
every thread"]) --> DOWN(["checkout starves too"]) end subgraph Bulk["Bulkheaded · isolated"] P1(["pool: checkout"]) --> OKC(["unaffected"]) P2(["pool: recommendations"]) --> SLOW(["only this degrades"]) end classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; classDef good fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef opt fill:#FEF3C7,stroke:#1F2937,color:#0E1726; class R1,DOWN,SLOW bad; class P1,OKC good; class P2 opt;
💡 In plain English

Ships have bulkheads: seal the hull into compartments and one breach floods one room, not the whole vessel. Separate pools are watertight compartments for your threads, and it's exactly the pool at 02:14 that sank checkout.

The highest-signal sentence · 55–75 min

Fail-open vs fail-closed: decide per path, out loud.

When a dependency dies, the path either keeps working permissively (fail-open, favours availability) or refuses safely (fail-closed, favours safety). There is no global answer, it's per path. Saying which, and why, is the highest-signal sentence in a design review.

flowchart LR
  AUTH(["Auth check down"]) --> AC(["FAIL CLOSED:
deny access
safety beats uptime"]) REC(["Recommendations down"]) --> RO(["FAIL OPEN:
show generic list
uptime beats perfection"]) PAY(["Fraud score down"]) --> PC(["FAIL CLOSED:
hold the charge
money is safety-critical"]) classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; classDef good fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef opt fill:#FEF3C7,stroke:#1F2937,color:#0E1726; class AUTH,PAY bad; class AC,PC bad; class REC opt; class RO good;
🔥 Same word, opposite right answer

Knight Capital, 2012: a bad deploy fired live trades with no kill switch — ~$440M gone in 45 minutes. Cloudflare, 2019: one unbounded regex failed the whole edge closed — global 502s in minutes. Wrong mode, either way, is fatal.

Drill · say it out loud · 5 min

Now say yours. Round the room.

⏱ everyone names one dependency — no "it depends"
The sentence
"When [ this dependency ] fails, this path fails [ open / closed ], because [ one reason ]."
Worked examples
When the fraud service fails, checkout fails closed, because releasing money unscored is worse than a slow checkout.
When the avatar CDN fails, the profile page fails open, because a missing picture beats a blank page.
💡 Why we rehearse it

This exact sentence is the staff move in a real design review. Saying it under mild pressure now is how it becomes reflex when it counts.

Degradation modes · 75–90 min

Degrade on purpose. Partial service beats a blank page.

When you can't serve the perfect response, serve a useful one. Degradation is a designed mode, not an accident: decide in advance what you drop first and what you protect to the last. The user should notice less, not everything at once.

A
Serve stale
last-known-good from cache beats an error
B
Shed load
drop low-value traffic to protect the core path
C
Drop features
hide the optional, keep the essential working
D
Read-only mode
stop writes, keep serving reads when the DB is strained
💡 In plain English

When a kitchen gets slammed it doesn't shut, it trims the menu to the dishes it can still cook well. Customers get fed, just fewer choices. That's graceful degradation.

Case closed · the 2:14am incident, solved

Cascading failure: how local becomes global.

Here is the cold open, drawn out. A single slow dependency fills retry queues, the retries exhaust the shared pool, callers time out and retry harder, and the slowness climbs the stack until the whole site is down, often well after the original fault healed. Every pattern tonight is a firebreak that stops the climb at one hop.

flowchart LR
  D(["one DB slows"]) --> Q(["retries pile up"])
  Q --> P(["shared pool exhausted"])
  P --> T(["callers time out,
retry harder"]) T --> G(["site-wide outage"]) P -. "bulkhead" .-> FB1(["pool stays local"]) Q -. "breaker + jitter" .-> FB2(["storm never forms"]) classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; classDef good fill:#D6F5E3,stroke:#1F2937,color:#0E1726; class D,Q,P,T,G bad; class FB1,FB2 good;
💡 In plain English

One stalled car becomes a five-mile jam because everyone behind brakes and reacts. Breakers, bulkheads and jitter are the hard shoulder and the merge lights: they keep one stall from freezing the whole motorway.

Putting it together · a mini incident

Worked example: the toolkit on one real system.

A booking app (think the ClubCP case study): web → API → bookings DB, with an email provider and a recommendations service hanging off the side. The email provider goes slow. Walk the toolkit and the failure stays a footnote instead of a front-page outage.

flowchart TB
  WEB(["Web"]) --> API(["API"])
  API --> DB(["Bookings DB
hard edge · fail closed"]) API --> EMAIL(["Email provider
soft · timeout 800ms"]) API --> REC(["Recommendations
soft · fail open"]) EMAIL -. "slow" .-> BRK(["breaker trips →
queue email, return 200"]) REC -. "down" .-> DEG(["serve generic list"]) classDef ok fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef io fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef good fill:#D6F5E3,stroke:#1F2937,color:#0E1726; class WEB,API,DB ok; class EMAIL,REC io; class BRK,DEG good;
💡 In plain English

The booking still confirms. The receipt email is queued for later (breaker + bulkhead kept it off the critical path), the "you might also like" quietly falls back (fail open), and the booking write itself is protected (fail closed). One dependency limped, nobody lost a booking.

Not theory · these actually happened

Four outages, four missing patterns.

AWS S3 · 2017
A typo took S3 offline in us-east-1 for ~4 hours; the status page depended on S3 and stayed green.
nobody knew their blast radius
AWS DynamoDB · 2015
A retry storm kept re-killing an overloaded metadata service for hours.
retry without jitter
Knight Capital · 2012
A bad deploy fired live trades with no kill switch. ~$440M gone in 45 minutes.
no circuit breaker
Cloudflare · 2019
One unbounded WAF regex pinned every CPU. Global 502s in minutes.
nothing was bounded
💡 In plain English

Every pattern tonight is a scar. Somebody shipped the version without it, at scale, and wrote the postmortem so you don't have to.

Review drill · find the four sins · 3 min

You're reviewing this config. What's wrong?

⏱ 3 minutes — four defects hiding in plain sight
# PaymentService · resilience.yaml (up for review)

http:
  connect_timeout: none
  read_timeout:    none

retry:
  attempts:  5
  backoff:   fixed 200ms
  retry_on:  [500, 503, 400, timeout]

breaker:
  trip_after: 20 failures
  reset:      manual

pools:
  mode:    shared
  size:    200 threads
  used_by: [fraud, ledger, email, recs]
Review drill · the answers

The four you should have circled.

No timeout
connect + read: none — one hang holds the thread and the user forever
Retry without jitter
fixed 200ms, and it retries a 400 — a synchronized storm on a non-retryable error
Breaker with no half-open
reset: manual — it trips open and stays dark until a human wakes up
One shared pool
200 threads for fraud + ledger + email + recs — the slowest starves checkout
💡 In plain English

These aren't exotic. They're the default you get when nobody decided. Naming them in a review is cheap insurance, and it's exactly the staff-level instinct we're building.

Recap · then what's next

Contain the blast. Choose the failure mode.

Assume everything fails, map the blast radius, and bound every call with timeouts, retries with jitter, breakers and bulkheads. Then decide fail-open vs fail-closed per path, out loud, and design the degradation modes before you need them.

Project 4 · ADR #3 — Capstone Decision
Resilience is a first-class part of the capstone. Due Jul 6.
feeds capstone
Take-home
Blast-radius map your capstone: label every edge hard/soft, pick the one hard edge you'd soften first, and write the fail-open/closed sentence for your riskiest dependency.
bring it to S10