Designing for Failure: A Backend Engineer's Mental Model
Designing for failure means assuming that every dependency can fail and building the application to degrade gracefully rather than cascade. Timeouts. Retries with backoff. Circuit breakers. Bulkheads. Graceful degradation. Each is a small pattern that prevents a small failure from becoming a customer facing incident. The mental model is that failures are normal and the system should handle them as such.
Written by Yashveer Singh, founder of Yashveer Labs.
What you actually need to know
- Every external dependency can fail. Design for it.
- Timeouts on every call. Default ones are too long.
- Retries with exponential backoff and maximum attempts. Idempotency required.
- Circuit breakers stop calling failing dependencies.
- Bulkheads isolate failure. Graceful degradation preserves the user experience.
| Pattern | What it prevents |
|---|---|
| Timeout | Hung callers |
| Retry with backoff | Cascading failures |
| Circuit breaker | Capacity exhaustion |
| Bulkhead | Failure spreading |
| Graceful degradation | Customer facing breakage |
| Idempotency | Duplicate side effects on retry |
| Dead letter | Lost work on terminal failure |
| Backpressure | Producer overrunning consumer |
The core argument
Production systems fail. Not occasionally. Often. Dependencies go slow. Networks partition. Disks fill. Services restart. The teams that pretend failure is exceptional ship systems that fail badly when reality intrudes. The teams that design for failure ship systems that degrade gracefully and recover automatically.
The mental model is small. Every external call can fail. The application has to handle it. The handling is a small set of patterns applied consistently. Timeouts on every call. Retries with backoff. Circuit breakers for repeat failures. Bulkheads to isolate. Graceful degradation to preserve customer experience.
The cost of the patterns is small. The libraries exist. The application code adds a few lines per call site. The reasoning is the discipline. The team has to think about what happens when the call fails. The answer should be specific to the call. A retry might be right. A circuit breaker might be right. A graceful fallback might be right.
The teams that do this well have production systems that survive dependency failures the customer never sees. The teams that do not have systems that cascade. A slow third party API takes down the application. A database hiccup propagates to every endpoint. The team learns by causing incidents.
The patterns reference
| Pattern | Implementation |
|---|---|
| Timeout | Per call, set deliberately. Usually a few seconds for hot path. |
| Retry with backoff | Exponential. Max attempts. Idempotent required. |
| Circuit breaker | After N failures in a window, stop calling. Half open test to recover. |
| Bulkhead | Separate resource pools per dependency. |
| Graceful degradation | Fallback that is worse but not broken. |
| Idempotency | Server side support so retries are safe. |
| Dead letter | Failed work captured for later inspection. |
| Backpressure | Producer slows when consumer is overwhelmed. |
| Compensating transaction | Roll back a side effect when later step fails. |
| Hedged requests | Send to multiple replicas, take the first. |
How much does this cost
The cost is small once the patterns are in the team's repertoire. A few hours per critical surface to apply them. The libraries do the heavy lifting. The reasoning is the discipline. The cost is mostly the time to think about failure, not the code to handle it.
Features the failure design must have
- Timeouts on every external call.
- Retry policy documented per call site.
- Circuit breakers on the highest stakes dependencies.
- Bulkheads where dependencies could starve each other.
- Graceful fallbacks for user facing features.
- Idempotency on write endpoints.
- Dead letter handling for background work.
- Monitoring on timeout rate, retry rate, breaker state.
- Chaos testing of failure modes.
Expert opinion
The teams that design for failure ship systems that survive the kinds of incidents that take down their peers. The patterns are small. The discipline is to apply them consistently. The teams that do not design for failure spend years learning the patterns by causing incidents. The cost of incidents is large. The cost of applying the patterns by default is small.
>
Yashveer Singh, founder of Yashveer Labs
How this played out on a real project
A client SaaS experienced a major incident when their payment processor went slow. The application called the processor synchronously without a timeout. The slow processor calls held threads. The threads filled the application's pool. The whole application stopped responding. The incident lasted four hours.
We applied the failure patterns. Timeout on every external call. Circuit breaker on the payment processor. Graceful degradation that put orders into a pending state when the processor was unavailable. Dead letter queue for the orders that needed manual reprocessing.
A similar event six months later was contained. The circuit breaker opened. The application kept serving other traffic. Orders went into the pending state. The processor came back online. The pending orders were retried. The customers saw a brief delay on payment confirmations instead of an outage.
For more on the related work, see resilience patterns circuit breakers retries bulkheads and timeouts the setting most engineers get wrong.
Common mistakes teams make
- Default timeouts. Usually too long.
- No retry policy. Or retries without backoff.
- No circuit breakers on dependencies that fail.
- No bulkheads. One dependency starves others.
- No graceful degradation. Features break entirely on dependency failure.
- No idempotency. Retries duplicate side effects.
- No dead letter handling. Failed work disappears.
- No chaos testing. Failure modes are theoretical.
A 30 day plan to put patterns in place
- Week one. Audit external calls. Identify the ones without timeouts.
- Week two. Add timeouts. Add retries where appropriate.
- Week three. Add circuit breakers on high stakes dependencies.
- Week four. Add graceful fallbacks for user facing features. Run chaos tests.
For more on the related work, read resilience patterns circuit breakers retries bulkheads and chaos engineering at startup scale. On the broader reliability side, the outbox pattern a SaaS reliability cheat code is the natural next read.
Frequently asked
About the author and why it matters
Yashveer Singh wrote this. I run Yashveer Labs out of New Delhi. The work I take on tends to come from founders who have been burned by an agency, a freelancer, or their own ambition. I do not promise miracles. I promise that the system will be online, the code will be readable, and the next engineer who touches it will not curse me. That is rarer than it should be.
Posts that line up with this one.
- Backend, APIs, and System Design
Async Job Failure Recovery: Patterns That Actually Work
Every async job will fail eventually. The patterns that recover gracefully are the ones I drop into every client project. Here are the five that matter and the order to add them.
- Backend, APIs, and System Design
Event Sourcing: A Pattern Worth Understanding Even If You Do Not Use It
Event sourcing is one of those patterns that shapes how you think about state even when you do not adopt it directly. The concepts are valuable. The full implementation is rare. Here is the honest read.
- Backend, APIs, and System Design
CQRS in Practice: When the Complexity Earns Its Keep
CQRS is a pattern that earns its complexity in specific cases and costs more than it gives in most. Here is the honest read on when separating reads from writes pays back.
- Backend, APIs, and System Design
ACID vs BASE: When Each Belongs in Your Architecture
ACID and BASE are not religions, they are tools. Picking the wrong one costs you data integrity or performance. Here is the call I make for client projects, and the reasoning behind each side.