Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Designing for Failure

Failure is not an option—it's inevitable. Design for it.
Diagram

In distributed systems, failure is not a possibility—it’s a certainty. Servers crash unexpectedly. Networks partition. Databases become overloaded. Disks fill up. Memory leaks occur. Dependencies fail at the worst possible moments. The question isn’t if your system will fail, but when and, more importantly, how gracefully it handles that failure.


Diagram

Designing for failure is a fundamental mindset shift in software engineering. Instead of assuming everything will work perfectly, you assume everything will fail and build systems that continue operating despite those failures.

Imagine building a house. Without failure design, one broken pipe floods the entire house, a power outage means no lights or heat, and one broken window makes the house unusable. With failure design, you have multiple pipes with shutoff valves for isolation, a backup generator for redundancy, and windows that can break without affecting the rest of the house—graceful degradation.

Designing for failure in software works the same way. You build systems that continue working even when parts break. This means adding redundancy, implementing timeouts, using retries, deploying circuit breakers, enabling graceful degradation, and setting up comprehensive monitoring.


The first and most important principle is to assume everything will fail. This isn’t pessimism—it’s realism based on decades of production experience. Networks timeout. Services crash. Databases overload. Disks fill up. Memory leaks occur. Dependencies fail.

Diagram

When you assume everything will fail, you design differently. You add redundancy so one failure doesn’t bring down the system. You implement timeouts so slow operations don’t hang forever. You use retries for transient failures. You deploy circuit breakers to prevent cascade failures. You enable graceful degradation so the system continues operating with reduced functionality. You set up monitoring so you know when things break.

The key mindset shift is moving from “This service should always work” to “This service will fail—how do we handle it?” This shift changes how you write code, how you design systems, and how you think about reliability.

Fail fast means detecting failures quickly rather than waiting indefinitely. If a service is down, don’t wait 30 seconds for a timeout—detect it in 1 second and move on. If a database query is slow, don’t let it hang—set a timeout and fail fast.

Diagram

Fail loud means making failures visible. Log errors clearly. Set up alerts for failures. Make failures observable. You can’t fix what you don’t know is broken. When something fails, you want to know about it immediately, not discover it hours later when users complain.

The combination of failing fast and failing loud means your system detects problems quickly and makes them visible, allowing you to respond rapidly and prevent small failures from becoming big problems.

Isolation prevents cascading failures. When one service fails, you want to prevent that failure from spreading to other services. This is where circuit breakers, bulkheads, and other isolation patterns come in.

Diagram

Without isolation, a failure in Service A can overload Service B, which then crashes Service C, bringing down the entire system. With isolation, when Service A fails, a circuit breaker opens, preventing requests from reaching it. Service B and C continue operating normally, protected from the failure.

Isolation is critical in distributed systems where failures are inevitable. By isolating failures, you prevent one broken component from taking down the entire system.


Diagram

Understanding common failure modes helps you design for them proactively. Here are the most common types of failures you’ll encounter:

Network failures include timeouts, connection refused errors, network partitions, and slow responses. These are among the most common failures in distributed systems. Design for them with timeouts, retries, and circuit breakers.

Service failures include service crashes, out of memory errors, CPU overload, and unhandled exceptions. Services crash for many reasons—bugs, resource exhaustion, configuration errors. Design for them with health checks, auto-restart mechanisms, and graceful shutdown procedures.

Dependency failures occur when external APIs go down, databases become overloaded, caches become unavailable, or third-party services fail. These failures are outside your control, so you must design for them with fallbacks, caching strategies, circuit breakers, and graceful degradation.

Resource exhaustion happens when disks fill up, memory leaks occur, connection pools are exhausted, or thread pools are full. These failures often indicate scaling issues or bugs. Design for them with resource limits, monitoring, and auto-scaling.

Cascading failures occur when one failure causes another, creating a chain reaction that can bring down entire systems. These are the most dangerous failures because they can cause system-wide outages. Design for them with isolation patterns, circuit breakers, bulkheads, and rate limiting.


Real-World Scenario: Netflix and Chaos Engineering

Section titled “Real-World Scenario: Netflix and Chaos Engineering”
Diagram

Netflix is a prime example of designing for failure. They run their entire streaming service on AWS, which means they’re subject to all the failures that cloud infrastructure can experience. Instead of hoping failures won’t happen, they actively test for them through chaos engineering.

Netflix developed several chaos engineering tools. Chaos Monkey randomly terminates instances in production to ensure their system can handle server failures. Latency Monkey introduces random network latency to test how the system handles slow networks. Chaos Gorilla simulates entire availability zone failures to test regional resilience.

The principles Netflix follows are:

  1. Start small - Begin with non-critical services to build confidence
  2. Gradually increase - As you learn and improve, increase the scope of chaos experiments
  3. Monitor everything - Watch metrics closely during chaos experiments to understand impact
  4. Automate - Make chaos experiments repeatable and automated
  5. Learn and improve - Fix weaknesses discovered through chaos engineering

This approach has helped Netflix build one of the most resilient systems in the world. When AWS has outages, Netflix often continues operating because they’ve tested and designed for those exact failure scenarios.


Graceful degradation means the system continues operating with reduced functionality when components fail. Instead of showing an error page or crashing, the system adapts to the failure and continues serving users, albeit with limited features.

Real-World Example: E-commerce Site During Database Failure

Section titled “Real-World Example: E-commerce Site During Database Failure”

Consider an e-commerce site during a database failure. Without graceful degradation, the entire site goes down—users can’t browse products, can’t see recommendations, can’t view reviews, and can’t check out. With graceful degradation, the site continues operating:

Diagram
  • Show cached catalog - Product information is served from cache, even if it’s slightly stale
  • Show cached recommendations - Recommendation engine uses cached data
  • Show cached reviews - Reviews are served from cache
  • Queue orders for later - Orders are queued and processed when the database recovers
  • Show “checkout unavailable” - Users are informed that checkout is temporarily unavailable, but they can still browse

This approach keeps the site functional during failures, providing a much better user experience than a complete outage.

Strategies for graceful degradation:

  • Cached data - Serve stale but available data when primary sources fail
  • Reduced features - Disable non-critical features to keep core functionality working
  • Queue operations - Queue writes for later processing when systems are down
  • Partial results - Return what’s available rather than failing completely
  • User notification - Inform users of limitations so they understand what’s happening

Diagram

Defensive programming is writing code that handles unexpected inputs, failures, and edge cases. It’s about assuming things will go wrong and handling those cases gracefully.

Always validate inputs, even from trusted sources. Input validation prevents bugs, security vulnerabilities, and unexpected behavior. Validate required fields, types, ranges, and business rules.

Real-world example: A payment processing system receives an order with a negative total amount. Without validation, this could cause accounting errors or security issues. With validation, the system rejects the invalid input immediately with a clear error message.

Handle exceptions gracefully with proper logging and fallbacks. Different exceptions require different handling—some are retryable, some require fallbacks, and some should fail fast.

Real-world example: A user service tries to fetch user data from the database. If the database connection fails, it falls back to cache. If cache also fails, it returns a graceful error rather than crashing. This ensures the system continues operating even when dependencies fail.

Always set timeouts to prevent hanging operations. Without timeouts, a slow database query can hang forever, consuming resources and blocking other requests. With timeouts, operations fail fast, freeing resources quickly.

Real-world example: An API call to an external service hangs. Without a timeout, the request waits indefinitely, consuming a thread and potentially blocking other requests. With a 5-second timeout, the request fails fast, the thread is freed, and the system can handle other requests.


You can’t fix what you don’t know is broken. Comprehensive monitoring and alerting are essential for detecting failures quickly and responding appropriately.

Diagram

Availability measures uptime percentage. Target 99.9% availability means the system can be down for about 8.76 hours per year. Higher availability targets (99.99%, 99.999%) require more sophisticated failure handling.

Error rate measures errors per second. A high error rate indicates problems. Target less than 0.1% error rate for most systems.

Latency measures response time. Monitor P50, P95, and P99 latencies. P99 latency under 1 second is a common target for most APIs.

Throughput measures requests per second. Monitor for drops in throughput, which can indicate problems before they become failures.

Health checks monitor service health, including CPU usage, memory usage, disk space, and other resource metrics. Regular health checks help detect problems early.

Alert on failures - Set up alerts for error rates exceeding thresholds. Don’t wait for users to report problems.

Track SLAs - Monitor availability and latency against your service level agreements. Know when you’re violating SLAs.

Health checks - Implement regular health check endpoints that services can call to verify they’re healthy.

Distributed tracing - Track requests across services to understand failure propagation and identify bottlenecks.

Log aggregation - Centralize logging for easier debugging. When something fails, you need logs to understand what happened.


Assume Failure

Everything will fail. Design for it from the start. Don’t assume services are always available.

Fail Fast

Detect failures quickly. Set timeouts. Use circuit breakers. Don’t wait indefinitely.

Isolate Failures

Prevent cascading failures. Use circuit breakers, bulkheads, and isolation patterns.

Graceful Degradation

Continue operating with reduced functionality. Serve cached data, disable non-critical features.

Chaos Engineering

Test resilience by injecting failures. Find weaknesses before real failures occur.

Monitor Everything

Track availability, error rates, latency. Set up alerts. You can’t fix what you don’t know is broken.



  • “Release It!” by Michael Nygard - Production-ready software design
  • “Site Reliability Engineering” by Google - SRE practices and principles
  • Netflix Chaos Engineering - Chaos engineering principles and tools
  • “Building Microservices” by Sam Newman - Resilience patterns for microservices
  • “Designing Data-Intensive Applications” by Martin Kleppmann - Fault tolerance and reliability