Blog 22
Designing for Failure
You cannot prevent failure. Machines die, networks partition, dependencies time out, disks fill, providers go dark — always, eventually, at the worst moment.
So the goal was never a system that doesn't fail.
It's a system that fails small, fails loud, and stays standing.
Twenty-one articles built systems. Nearly every one of them named a failure in passing and pointed here. The retry storm that amplifies an outage (Bonus E). The cache stampede that flattens a database (Part 6). The split-brain that consensus exists to prevent (Part 20). The bulkhead that keeps a campaign from burying an OTP (Part 14). The graceful degradation that beats a 500 (Part 10). The compensation that undoes what can't be rolled back (Parts 18, 21). Each was a fragment of one discipline, deferred to here.
This is that article. It is not a new category of system — it's the discipline that runs through all of them: the deliberate art of designing for the failures you cannot prevent. Section 2 taught you to build systems. Section 3 taught you to build categories. Section 4 taught you the hardest problems. This article teaches the thing underneath all of it — how to keep a system standing when the things it depends on don't.
Same walkthrough spirit, one altitude up: the "system" here is any system, and the requirement is survival.
Why Designing for Failure Is the Real Job
Failure is the default, not the exception. Part 2 opened the series with it: at scale, something is always broken. A system of a hundred services where each is 99.9% reliable is, multiplied out, failing somewhere almost constantly. Designing for the happy path is designing for a world that doesn't exist. The resilient engineer inverts the question — not "does this work?" but "what happens when this dependency doesn't?"
Most outages are amplified, not caused. This is the deepest lesson in the article. A single slow dependency rarely takes down a system by itself — it takes it down because the system reacts badly: retries pile on, threads block, resources exhaust, and one sick component becomes a total outage. The failure was small. The response to the failure was the disaster. Resilience is mostly about not making a small failure worse.
The patterns are few, and you've already met them. Timeouts, retries with backoff, circuit breakers, bulkheads, fallbacks, graceful degradation, idempotency, redundancy — a compact toolkit, scattered across twenty-one articles, that this one finally assembles into a coherent whole. There's no exotic magic here, only the discipline of applying known patterns before the incident.
And it's what separates senior engineers. Anyone can design the happy path. The engineer who instinctively asks "what happens when this times out? what's the fallback? what's the blast radius?" — before writing the code — is the one whose systems survive Tuesday. Designing for failure is the clearest marker of system-design maturity there is.
The Simple Mental Model: A Building Designed for Fire
You cannot prevent every fire. So buildings aren't designed to never burn — they're designed so that when a fire starts, it stays contained and everyone gets out.
Fire doors stop a fire in one room from spreading to the whole building — bulkheads, doing isolation. Circuit breakers cut power to a failing circuit before it burns the wiring — literally the name of the pattern, borrowed whole. Sprinklers and alarms detect and respond automatically — health checks and automated failover. Fire exits let people leave even when the main path is blocked — fallbacks and graceful degradation. Fire drills prove the plan works before the real fire — chaos engineering and game days.
Nobody calls a building with fire doors "pessimistic." They call it up to code. Resilience engineering is the same: not paranoia, but the professional standard for systems that carry real weight. You design for the fire you know will eventually start.
Timeouts: The Most Underrated Resilience Pattern
Start with the humblest and most violated pattern, because everything else builds on it.
Every call to anything that can hang must have a timeout. A network call, a database query, a dependency request — without a timeout, a hung dependency doesn't just fail that one call; it holds the calling thread forever, and threads are finite.
Walk the cascade this prevents:
No timeout on a call to a slow dependency:
→ the calling thread blocks, waiting… forever
→ more requests arrive, each blocking its own thread
→ the thread pool exhausts
→ the service can no longer serve ANY request — even ones
that don't touch the slow dependency
→ a slow DEPENDENCY became a total OUTAGE of a healthy service
That is the single most common way a small failure becomes a large one — and a timeout stops it at the first line. A call without a timeout is a thread leak with good intentions (Part 10 said it; here's why it matters).
The subtlety: timeouts must be tuned, not guessed. Too long, and they don't protect you (the thread still blocks for ages). Too short, and you abort requests that would have succeeded, manufacturing failures. Set them from real latency data — the p99 you measured in Part 12 — with a margin, not from a hopeful round number. And timeouts compose: a chain of services each waiting on the next means the outermost timeout must account for the whole chain (Bonus A's latency budget, now a resilience constraint).
Retries: Necessary, and Dangerous
A timeout tells you a call failed. Often the right response is to try again — the failure was transient (a blip, a brief overload). But retries are the pattern most likely to cause the outage they were meant to survive.
The Retry Storm (Bonus E, Formalized)
A dependency slows down under load.
→ callers time out and RETRY
→ retries ADD traffic — exactly when the dependency is already struggling
→ more load → more timeouts → more retries → collapse
→ the retries, not the original slowness, killed it
This is the amplification lesson made concrete: naive retries turn a partial degradation into a total outage. The dependency might have recovered on its own; the retry storm ensured it couldn't.
The Disciplines That Make Retries Safe
Exponential backoff waits longer between each attempt — one second, then two, then four — giving the dependency room to recover instead of hammering it. Jitter randomizes the backoff, so a thousand clients that failed together don't retry in synchronized waves (Part 15's reconnect-stampede fix, generalized) — without jitter, backoff just reschedules the stampede. Retry budgets cap retries as a fraction of total traffic (say, retries may add at most 10%), so retries can never dominate load — a per-request retry limit isn't enough; the budget is system-wide. And retry only what's safe: here the whole series pays off, because retrying is only safe if the operation is idempotent (Parts 7, 14, 21) — retry a read freely, retry a payment only with an idempotency key, since a retry without idempotency is a double-charge waiting to happen.
The rule: a retry is a bet that the system has spare capacity. During a failure, it usually doesn't — so bet carefully, with backoff, jitter, a budget, and idempotency, or don't bet at all.
Circuit Breakers: Failing Fast on Purpose
Retries handle transient failures. But when a dependency is genuinely down — not blipping, down — continuing to call it (even with backoff) wastes resources, blocks threads, and delays the inevitable. The circuit breaker is the pattern that says: stop trying, fast, until it's likely to work again.
It's a state machine modeled exactly on the electrical device:
CLOSED — calls flow normally; failures are counted
│ failure rate crosses a threshold
▼
OPEN — calls FAIL IMMEDIATELY without even trying the dependency
│ (fast failure, no blocked threads, no wasted calls,
│ the struggling dependency gets breathing room to recover)
│ after a cooldown timer
▼
HALF-OPEN — allow a FEW trial calls through
succeed → back to CLOSED (recovered)
fail → back to OPEN (still down, wait again)
Three things the breaker buys you. It stops the retry storm at the source — an open breaker means no calls, so no amplification; it's the retry discipline's essential partner. It fails fast — a request that would have hung for a timeout instead returns immediately, freeing the thread to serve requests that don't need the dead dependency. And it gives the dependency room to recover — the most humane thing you can do for an overloaded service is stop calling it, and the breaker enforces that automatically.
The breaker turns "hammer the corpse until we both die" into "step back, check periodically, resume when it's alive." And critically, an open breaker is where a fallback belongs: not "fail," but "fail to something."
Bulkheads: Containing the Blast Radius
Named for a ship's watertight compartments: puncture one, and the sea floods that compartment only — the bulkheads keep the rest dry, and the ship stays afloat. In software, the bulkhead isolates resources so that one overloaded component can't drown the whole system.
Part 14 built this without fully naming it — transactional and promotional notifications in separate lanes, separate queues, separate workers, so a five-million-message campaign couldn't starve the OTPs. That's a bulkhead: the campaign floods its compartment; the OTP compartment stays dry.
The pattern generalizes everywhere resources are shared:
WITHOUT bulkheads (one shared pool):
dependency-A goes slow → all threads block on A →
requests for dependency-B and C can't get a thread either →
ONE slow dependency takes down EVERYTHING
WITH bulkheads (isolated pools):
dependency-A gets its own thread pool / connection pool / queue →
A goes slow → A's pool exhausts → B and C keep serving normally →
the failure is CONTAINED to the compartment that flooded
Bulkheads apply to thread pools (a pool per dependency), connection pools (Part 9's stampede, contained), queues (Part 14's lanes), and even whole service tiers (isolate the critical path from the best-effort path). The trade (Bonus C): isolation costs some efficiency — dedicated pools are less flexible than one shared pool — but it buys blast-radius containment, and for anything critical, that trade is almost always right.
The principle in one line: a failure should be able to sink one compartment, never the ship.
Graceful Degradation and Fallbacks
The patterns so far contain failure. This one makes failure survivable to the user — the system loses a capability without losing the whole experience.
Graceful degradation is the difference between "the feature is unavailable" and "the site is down." When a dependency fails, a resilient system degrades to a lesser-but-working state instead of collapsing: a recommendations service down shows a generic popular-items list, not an error. Personalization unavailable serves the non-personalized default. A live inventory count timing out shows "in stock" without the exact number. Search ranking degraded falls back to simpler ranking (Blog 16). Rich profile data loading slowly renders the core page while extras load async. A payment provider going down fails over to a second provider (Part 14).
Every one of those is a fallback — a designed "what to do instead" when the primary path fails. The pattern appeared across the series (Part 10's "partial answer beats a 500," Part 16's ranking degradation, Part 17's ABR dropping quality instead of stalling) — now named as one discipline.
The design questions that make it real, asked per dependency, in advance: if this fails, what's the degraded experience — not "an error," a lesser working state? Is there a fallback data source — a cache, a default, a stale-but-usable copy? And which dependencies are critical (no meaningful fallback exists — the system genuinely can't function) versus enhancing (a fallback exists — the system works, just less richly)?
That last distinction is the heart of it: most dependencies are enhancing, not critical — and a resilient system is one that has honestly sorted which is which, and degrades the enhancing ones instead of dying with them. The failure everyone remembers is the recommendation service taking down the whole store; the fix was always "recommendations are enhancing — degrade, don't die."
Redundancy, Failover, and the Single Point of Failure
The bluntest resilience pattern: have more than one of anything whose loss would stop the system.
A single point of failure (SPOF) is any component whose death takes everything down — one database with no replica, one load balancer, one region, one nameserver (Bonus D's cruelest outage). The discipline is to find them before they find you: walk the architecture and ask, of every box, "what happens when this one dies?" The boxes with no good answer are your SPOFs.
The series already built the answers. Replication (Bonus I, Part 20) keeps N copies so one death loses nothing. Health-checked failover (Bonus E) detects death and reroutes to a healthy replacement, automatically. Multi-AZ and multi-region design (Bonus D, Part 20) survives the loss of a whole failure domain. And quorums (Part 20) keep serving when a minority dies.
The essential subtlety, learned expensively across the industry: failover that has never been tested is not failover — it's a hope with a config file. A standby that's never been promoted, a backup that's never been restored (Part 8), a multi-region setup whose failover was never drilled — all of them fail in the one moment they're needed, because their first real exercise is the disaster itself. Which is exactly why the last pattern exists.
Chaos Engineering: Testing Failure on Purpose
Every pattern above is a claim: "the timeout will protect us," "the breaker will trip," "the failover will work." Chaos engineering is the discipline of verifying those claims — deliberately injecting failure into a system to prove it survives, before reality tests it for you.
It sounds reckless; it's the opposite. It's the fire drill — the controlled, planned exercise that finds the gaps while everyone's watching and calm, instead of at 3 AM in a real incident:
The chaos experiment:
1. form a hypothesis — "if dependency X dies, the breaker trips and
we degrade to the cached fallback; users see a slower but working site"
2. inject the failure — kill X, in a controlled window, watching closely
3. observe — did the breaker trip? did the fallback engage? did the
blast radius stay contained? did observability actually show it?
4. fix what you learned — because something is always weaker than you thought
The mindset shift is the whole point: you do not know your system is resilient until you have made it fail on purpose. Every resilience pattern is untested code until chaos exercises it — and untested resilience code fails exactly when you need it. Game days (Part 12's Real-World Lens) are this discipline made routine: schedule the failure, run the drill, find the gap at 2 PM instead of meeting it at 2 AM.
Start small — one dependency, a controlled blast radius, a staging environment — build confidence, and graduate toward production experiments as trust grows. The goal is never chaos for its own sake — it's confidence, earned by evidence instead of hope.
Failure Analysis: The Patterns Working Together
Resilience patterns aren't a menu you pick one from — they compose, each covering another's gap. Walk one dependency failure through the full stack:
A downstream service (say, the recommendation engine) starts to fail.
1. TIMEOUT — calls to it abort in 200ms instead of hanging;
threads stay free (no exhaustion)
2. RETRY — a couple of jittered, budgeted retries catch a transient
blip; if it's not transient, they stop (budget/backoff)
3. CIRCUIT — sustained failures trip the breaker OPEN;
BREAKER calls now fail instantly, giving the engine room to recover
4. FALLBACK — the open breaker routes to a fallback: a generic
popular-items list from cache
5. BULKHEAD — because recommendations had its own thread pool,
none of this touched checkout, search, or payments
6. GRACEFUL — the user sees a working store with generic
DEGRADATION recommendations, never an error page
7. OBSERVABILITY — the breaker-open event, the fallback rate, and the
(Part 12) dependency's error rate all fire alerts; humans know
8. RECOVERY — the engine recovers; the breaker half-opens, tests,
closes; full service resumes automatically
Read that sequence and see the thesis: a small failure (one service degraded) stayed small. No thread exhaustion, no retry storm, no cascade, no user-facing outage, no 3 AM scramble — because eight patterns, each doing its narrow job, composed into a system that bent instead of breaking. Remove any one and the story gets worse: no timeout means thread exhaustion, no breaker means a retry storm, no bulkhead means checkout dies too, no fallback means error pages. The patterns are a system, not a list.
Common Mistakes Engineers Make with Resilience
Retries without backoff, jitter, or budgets — the single most common self-inflicted outage, retries amplifying the failure they meant to survive. Timeouts that are too long, or missing entirely — a 30-second timeout barely protects you, a missing one doesn't at all, and the thread-exhaustion cascade starts here. Retrying non-idempotent operations — the retry that double-charges, resilience creating a correctness bug (Part 21). Circuit breakers with no fallback behind them — failing fast into an error is better than hanging, but failing fast into a fallback is the actual goal; an open breaker with nothing behind it is just a faster error. No bulkheads, one shared pool for everything — the design where any single slow dependency takes down every unrelated feature. Treating every dependency as critical — if nothing has a fallback, everything is a SPOF, the failure to sort critical from enhancing. Untested failover and backups — the standby that's never been promoted, the backup never restored, resilience that exists only in the diagram. Resilience code that's never been exercised — every pattern above is untested until chaos runs it, and untested resilience fails when invoked. Cascading fallbacks that hide the failure — degrading silently with no alert, the system limping in a degraded state for hours because the fallback was too graceful and nobody noticed (observability, Part 12, is not optional). Over-engineering resilience for trivial systems — circuit breakers and bulkheads around a low-stakes internal tool nobody depends on, complexity the requirement never asked for (Bonus C's discipline: match the resilience to the stakes).
What Interviewers Actually Probe
"What happens when this dependency fails?" is the question you should be answering before it's asked, for every dependency you draw — volunteering timeout, retry, breaker, fallback, bulkhead unprompted is the strongest signal in the interview. "Your retries — talk me through them" wants backoff, jitter, budget, idempotency; naming all four (especially the budget and idempotency) marks someone who's survived a retry storm. "A downstream service gets slow — walk me through the blast radius" gets answered by timeouts preventing thread exhaustion, bulkheads containing it, breakers stopping the storm — "one slow dependency shouldn't take down unrelated features" is the sentence to say. "Which of your dependencies are critical versus enhancing?" is the degradation question; a candidate who has sorted them and designed fallbacks for the enhancing ones understands resilience structurally. "How do you know your failover actually works?" wants chaos engineering, game days, tested restores — "untested failover is a hope" is the senior answer. "Where are your single points of failure?" asks you to walk the architecture and name them honestly; a candidate who finds their own SPOFs is the one you trust.
The meta-move: treat every dependency as guilty until proven resilient. The senior instinct isn't "this works" — it's "this will fail; what happens then, and does the failure stay small?" Designing for the failure you know is coming is the entire discipline, and interviewers are listening for exactly that reflex.
From My Journey: An Architectural Lesson
For a long time, resilience feels like something you add after — you build the system, ship it, and then, once it breaks a few times, you start bolting on retries and timeouts and alerts, one incident at a time. Each patch is a scar from a specific outage: the retry logic added after the storm, the timeout added after the hang, the fallback added after the recommendation service took down the store.
And that reactive approach works, in the sense that the system slowly becomes more robust. But it works the expensive way — every improvement paid for with a real outage, real users affected, a real 3 AM.
The realisation that reorganizes the discipline: failure was never the exception to design around — it was the condition to design for. The dependencies will time out. The providers will go dark. The machines will die. A system built assuming they won't is a system that will be surprised, repeatedly, by the entirely predictable. And most of those outages were never caused by the failure itself — they were caused by the system reacting badly to a small failure it should have absorbed. The retry storm, the thread exhaustion, the cascade: self-inflicted, every time, by a system that turned a contained problem into a total one.
Which flips the whole question. Not "how do I keep this from failing?" — you can't — but "when this fails, does the failure stay small, stay loud, and stay contained?" A timeout so a hang doesn't cascade. A budget so a retry doesn't storm. A bulkhead so one flood doesn't sink the ship. A fallback so a lost feature isn't a lost system. A drill so the plan is real before the fire. None of it is exotic; all of it is the difference between a system that bends and one that breaks.
The lesson that stuck: resilient systems are not the ones that never fail. They are the ones where failure was expected, contained, and survivable by design.
Key Takeaways
- You cannot prevent failure — the goal is a system that fails small, fails loud, and stays standing.
- Most outages are amplified, not caused: a small failure becomes a large one because the system reacts badly. Resilience is mostly about not making it worse.
- Every call that can hang needs a tuned timeout — a call without one is a thread leak that turns a slow dependency into a total outage.
- Retries are necessary and dangerous: safe only with backoff, jitter, a system-wide budget, and idempotency — otherwise they are the outage.
- Circuit breakers fail fast on purpose — stopping the retry storm, freeing threads, and giving a down dependency room to recover.
- Bulkheads contain the blast radius: isolate resources so one flooded compartment can't sink the ship (Part 14's lanes, generalized).
- Graceful degradation sorts dependencies into critical vs enhancing, and degrades the enhancing ones to a lesser-but-working state instead of dying with them.
- Redundancy removes single points of failure — but failover that's never been tested is a hope with a config file.
- Chaos engineering verifies resilience by making the system fail on purpose — untested resilience code fails exactly when you need it.
- The patterns compose: timeout, retry, breaker, fallback, bulkhead, degradation, and observability together turn a small failure into a non-event.
Interview Lens
Resilience is less a section of the interview than a reflex the interviewer is listening for throughout.
Attach a failure answer to every dependency you draw. The moment you add a box, say what happens when it fails — "this call has a timeout and falls back to cache; if it's sustained, the breaker trips." Doing this unprompted, for every dependency, is the single clearest signal of a senior system designer. Most candidates design the happy path and wait to be asked about failure; the strong ones never stop asking it themselves.
Name the amplification. When you discuss a failure, mention that the danger is usually the response — "the risk isn't the slow dependency, it's the retry storm it triggers." Understanding that outages are amplified, not just caused, is the insight that separates mid from senior.
Compose the patterns, don't list them. Walk one failure through the stack — timeout, retry, breaker, fallback, bulkhead, degradation — and show them covering each other's gaps. The composition is the understanding; the list is just vocabulary.
And close with the honest one: "and I'd test it with a game day, because untested resilience is just a hope." That sentence, more than any pattern name, is what marks someone who has actually kept systems standing.
Real-World Engineering Lens
Audit your timeouts first — it's the cheapest, highest-leverage fix. Any call that can hang without a timeout is a latent total-outage. Find them, set them from your p99 data (Part 12), and you've closed the most common cascade.
Give every retry a budget and jitter, and mark which routes are idempotent. The retry storm is the most common self-inflicted outage; the fix is a config change and a discipline, not a rewrite.
Sort your dependencies into critical and enhancing, in writing. For each enhancing one, design the fallback now. The recommendation-service-takes-down-the-store outage is entirely preventable by this one exercise.
Find your SPOFs on purpose. Walk the architecture, ask "what dies if this dies," and fix the boxes with no good answer — before the incident finds them for you.
Run a game day this quarter. Kill a dependency in staging, watch what happens, and find the gap between your resilience diagram and your resilience reality. The gap is always there; the only question is whether you find it or the outage does.
What's Next
Stop and look at what these twenty-two articles built.
A mindset (1–3). The building blocks — data, caching, messaging, storage, compute, APIs, security, observability (4–12, and the method bonuses). Real systems, designed end to end — shorteners, feeds, chat, streaming, commerce, collaboration, infrastructure (13–20). The hardest correctness problem, payments (21). And now the discipline that keeps all of it standing when the world does what the world always does — fails (22).
You didn't learn a list of systems. You learned a way of thinking: clarify before designing, let requirements and estimates drive the architecture, choose consistency per data, name the trade-offs and who owns them, design the failures before they happen, and prove correctness instead of assuming it. That way of thinking transfers to any system, including the ones that don't exist yet.
The series has one more move — turning all of this from knowledge you have into judgment you use under pressure, in the room, on the whiteboard.
Blog 23 — The System Design Interview: Putting It All Together. Where the whole framework becomes a calm, repeatable method for the forty-five minutes that decide a role — and for every real design review after.
Question for Readers
Take the most important system you work on and pick its most important dependency.
Right now: what happens when that dependency fails? Is there a timeout? A fallback? A bulkhead? And has anyone ever tested it — or is the answer written only in the architecture diagram, never in reality?
If you can't answer with confidence, you've found your next design review. The failure is coming regardless. The only choice is whether you meet it by design or by surprise.