Back to Blog
Lesson 25 of the System Design: System Design Fundamentals course
ArchitectureAugust 11, 20264 min read

Designing for Failure: Resilience and Fault Tolerance Basics

Learn how to build resilient systems by implementing retry logic, circuit breakers, and graceful degradation to maintain reliability during component outages.

system designresiliencefault tolerancereliabilitycircuit breakers
Wooden letter tiles spelling 'failure' on a wooden table, representing challenges.

Previously in this course, we covered database partitioning and sharding to scale our data layer. Now that we have a distributed architecture, we must confront an uncomfortable reality: in a distributed system, components will fail.

Designing for failure means shifting your mindset from "preventing all errors" to "containing the blast radius." If a downstream service is down, your entire system shouldn't collapse with it.

The Three Pillars of Resilience

Resilience is the ability of a system to provide and maintain an acceptable level of service in the face of faults. We achieve this through three primary strategies:

  1. Retry Logic: Handling transient, short-lived errors.
  2. Circuit Breakers: Protecting your system from being overwhelmed by a failing dependency.
  3. Graceful Degradation: Providing a "good enough" experience when a feature is unavailable.

1. Implementing Robust Retry Logic

Not all errors are terminal. Network blips or temporary service restarts often cause 5xx errors or timeouts. A retry mechanism gives the system a second chance.

The Golden Rule: Always use Exponential Backoff with Jitter. Never retry immediately at a constant rate, or you risk turning a small hiccup into a self-inflicted Distributed Denial of Service (DDoS) attack.

PYTHON
import time
import random

def call_service_with_retry(func, max_retries=3):
    for i in range(max_retries):
        try:
            return func()
        except TemporaryError:
            # Exponential backoff: 1s, 2s, 4s + random jitter
            wait = (2 ** i) + random.uniform(0, 1)
            time.sleep(wait)
    raise Exception("Service failed after retries")

2. Preventing Cascading Failures with Circuit Breakers

While retries help with transient issues, they make matters worse if a service is permanently down. If you keep retrying against a dead service, you exhaust your own threads and memory.

A circuit breaker sits between your service and its dependency. It tracks failures. If the error rate exceeds a threshold, the "circuit opens," and all subsequent calls fail fast immediately—without wasting resources.

StateBehavior
ClosedRequests pass through normally.
OpenRequests fail fast; the system knows the dependency is down.
Half-OpenA limited number of requests are allowed through to test if the service has recovered.

3. Designing for Graceful Degradation

Graceful degradation is the UI/UX equivalent of a safety net. If your "Recommendations Engine" service fails, don't return an error page to the user. Instead, show a static list of popular items or hide the section entirely.

In our project, if the payment gateway is unreachable, we could potentially queue the order for later processing rather than failing the transaction entirely. This transforms a "system down" event into a "slightly delayed" event.

Hands-on Exercise: Implementing a Failure Fallback

Wooden tiles spell 'Fail Your Way to Success' emphasizing perseverance.

Imagine your application fetches user profile data from an external API.

  1. Create a function get_user_profile() that simulates a 50% failure rate.
  2. Wrap this in a simple retry loop (max 3 tries).
  3. Add a "fallback" block: if all retries fail, return a hardcoded "Guest" profile object instead of throwing an error.

Common Pitfalls to Avoid

  • Retrying non-idempotent operations: Never retry a POST request (like a payment or email send) without verifying if the previous attempt actually succeeded; you might double-charge the user. (We will cover this in our upcoming lesson on Idempotency in Distributed Systems).
  • Missing Timeouts: Every network call must have a hard timeout. A request that hangs forever is the fastest way to crash your application.
  • The "Retry Storm": If all your service instances start retrying at the exact same time, you create a spike of traffic that prevents the target service from ever recovering. Always add jitter.

FAQ

Q: When should I use a circuit breaker versus a retry? A: Use retries for transient errors (network flickers). Use a circuit breaker when you suspect the downstream service is overloaded or down for an extended period.

Q: Does graceful degradation mean I ignore errors? A: No. It means you handle them at the application level. Always log the error so your team can investigate, even if the user experience remains smooth.

Recap

Designing for failure is about building systems that stay upright when pieces of them fall down. By implementing intelligent retries, utilizing circuit breakers to stop cascading failures, and providing graceful fallbacks, you ensure high availability even in imperfect infrastructure.

Up next: Implementing Circuit Breakers — we will build a production-grade breaker for our running project.

Similar Posts