Engineering Atlas › System Design Interviews

Designing for failing components

13 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Redundancy in independent failure domains

Run at least N+1 (often N+2) instances of every stateless service across availability zones, sized so losing one zone leaves enough capacity. Stateful components need replication: a database primary with synchronous or semi-synchronous replicas in other zones, a consensus group of 3 or 5 nodes for coordination data, object storage with erasure coding across zones. Redundancy only helps if failures are independent, so watch for shared dependencies: one DNS provider, one config service, one certificate, one deploy pipeline. Pre-provisioning the spare capacity, rather than launching it after the failure, is what AWS calls static stability: the surviving zones need no control-plane call to cope (AWS Builders' Library).

Detection

A failure you do not detect becomes a hang. Every network call needs a timeout set from the dependency's measured latency (for example p99.9 plus margin), not a library default. Load balancers run active health checks and also passive outlier detection, ejecting hosts that return consecutive errors. Health checks should test what matters (can this instance serve a request) without depending on shared dependencies, or a database blip will mark the whole fleet unhealthy at once. Kubernetes warns that a careless liveness probe can cause cascading restarts under load, and puts back-end dependency checks in the readiness probe, not the liveness one (Kubernetes docs).

Isolation and containment

Degradation

Decide per dependency what happens without it. Recommendations fail: show popular items from a static list. Reviews fail: hide the reviews panel. Pricing fails: this is critical, so fail the request clearly. Precompute fallbacks so that the fallback path does not itself depend on something that might be down. Amazon goes further and avoids fallback logic where it can, because a rarely run path is the least tested code in the system; it prefers making the primary path more reliable or pushing data out ahead of time (AWS Builders' Library).

Failover for stateful parts

Leader-based stores elect a new leader when the old one stops renewing its lease. Failover must fence the old leader (with an epoch or fencing token) so a paused, not dead, primary cannot keep accepting writes. Asynchronous replication means failover can lose the last few seconds of writes; say so and decide whether that is acceptable.

How it shows up in interviews

Every design question has a reliability phase, and some are explicitly about it: design a highly available payment service, a distributed job scheduler, a key-value store, a notification system with delivery guarantees. A strong answer lists failure domains, gives each component a redundancy and failover story, sets timeouts and retries with budgets, adds circuit breakers and fallbacks, and discusses detection time and data loss on failover. Interviewers probe: what if the database primary dies mid-write, what if a dependency is slow rather than down, what if a whole zone goes, and how do you know a node is dead rather than slow.

Down the stack

Layer 1The contract

State availability and durability targets per component and per user journey: for example 99.95% success for checkout over 30 days, with recovery time under 60 seconds and zero acknowledged-write loss for orders. A circuit breaker promises that while open, calls fail within microseconds instead of waiting for a timeout, and that it will let a limited number of probe calls through after a cool-down. A health-checked load balancer promises to stop routing to a failed host within its detection window (for example 3 failed checks at 5-second intervals, so about 15 seconds). Synchronous replication promises an acknowledged write survives a single node loss; asynchronous replication does not.

Layer 2The mechanism

Envoy's outlier detection tracks per-host counters such as consecutive_5xx (5 by default) and ejects a host for base_ejection_time (30 seconds by default) multiplied by the number of prior ejections, capped by max_ejection_percent, 10% of the cluster by default, so it never ejects the whole cluster (Envoy docs). Envoy also supports retry budgets (retry_budget with budget_percent, 20% of active requests by default, Envoy docs), and gRPC clients have a retry throttling policy: each failure subtracts 1 from a token count that starts at maxTokens, each success adds tokenRatio, and retries stop while the count is at or below half of maxTokens (gRFC A6). A circuit breaker is a three-state machine: closed (count failures), open (reject until a timeout elapses), half-open (allow a probe; success closes, failure reopens). Leader election in etcd or ZooKeeper uses leases or ephemeral nodes; a client that loses its session must stop acting as leader, and the store hands the new leader a higher revision number usable as a fencing token. Kubernetes separates liveness probes (restart a wedged process) from readiness probes (stop sending traffic), which matters: failing liveness on a dependency outage restarts every pod at once.

Layer 3The machine

Without application timeouts, the kernel's defaults are slow. A connection attempt to a dead host retransmits SYNs with exponential backoff; with Linux's default tcp_syn_retries of 6, connect takes about 131 seconds to fail on current kernels. On an established connection, unacknowledged data is retransmitted up to tcp_retries2 (15 by default), a timeout of at least 924.6 seconds, about 15 minutes, before the socket errors. TCP keepalive defaults to 2 hours of idleness before the first probe (Linux kernel docs). A process paused by a long garbage collection, a VM live migration or a stalled disk can look dead for seconds and then resume, still believing it holds a lease, which is why fencing is needed. Disks fail in ways that are not crashes: an fsync that returns an error may have dropped dirty pages, and Postgres changed its behaviour to panic on fsync failure instead of retrying, after the 2018 "fsyncgate" discussion showed that a retry could falsely report success. The change shipped in February 2019 in 11.2 and the older supported branches, with data_sync_retry to restore the old behaviour (Postgres 11.2 release notes).

Why it's built this way

Hardware and networks fail independently and often at scale, so every component needs a peer in a separate failure domain. A slow component is indistinguishable from a dead one from the outside, so detection relies on timeouts tuned to real latency. Reactions to failure add load at the worst time, so retries and failovers are budgeted and damped. Paused processes can return, so leadership is enforced with fencing rather than assumed from silence.

Diagram

stateDiagram-v2
    [*] --> Closed
    Closed --> Closed: success or failure below threshold
    Closed --> Open: consecutive failures reach threshold
    Open --> Open: calls rejected fast
    Open --> HalfOpen: reset timeout elapsed
    HalfOpen --> Closed: probe succeeds
    HalfOpen --> Open: probe fails

Worked example

A checkout service runs 30 instances across three zones and calls pricing, inventory, payments and a recommendations widget. Each instance handles 100 requests per second.

Zone loss: losing one zone removes 10 instances. The remaining 20 must carry 3,000 requests per second, 150 each. The team sizes instances to run at 50% CPU normally, so 150 per instance is about 75%, survivable. Without that headroom, the survivors overload, latency climbs, health checks fail and the outage spreads to all zones.

Slow dependency: recommendations latency jumps from 30 ms to 4 seconds. The checkout call to it has a 150 ms timeout and a bulkhead of 20 concurrent calls per instance. Within a second, the breaker sees 20 consecutive timeouts and opens; for the next 30 seconds calls fail in microseconds and the page renders with a static "popular items" list. Every 30 seconds one probe tests recovery.

Retries: payments has a retry budget of 10%. During a 2-minute payments brownout where 40% of calls fail, naive single retries would add 40% more load to a struggling service; the budget caps the extra at 10%, and further failures surface as "please try again" with idempotency keys so user retries cannot double-charge.

Database failover: the orders primary uses semi-synchronous replication to one replica in another zone. When the primary's host dies, the orchestrator promotes the replica in about 20 to 30 seconds and bumps the epoch. Writes during that window fail and are retried by clients with idempotency keys.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Active-active replicasNo failover delay, uses all capacityNeeds headroom for loss, harder for stateful partsStateless services
Active-passive failoverSimple for stateful componentsDetection plus promotion delay, idle standbySingle-writer databases
Synchronous replicationNo acknowledged-write lossHigher write latency, availability tied to replicaMoney, orders
Asynchronous replicationLow latencyLoses recent writes on failoverFeeds, analytics, caches
Circuit breaker with fallbackFast failure, protects dependencyTuning, fallback may be staleOptional or degradable features

Pitfalls in production

Leadership lens

Check yourself

  1. Why can retries make an outage worse?

    Show answer

    They add load to a dependency that is already failing, and retries stacked across layers multiply, which can keep it from recovering.

  2. Why does a half-open state exist in a circuit breaker?

    Show answer

    It lets a small number of probe calls test whether the dependency has recovered without sending full traffic to it.

  3. Why is a health check that queries the shared database risky?

    Show answer

    A brief database problem makes every instance fail its check simultaneously, so the load balancer removes the entire fleet.

  4. What does a fencing token prevent?

    Show answer

    It stops a paused former leader from writing after a new leader has taken over, because storage rejects writes with an older token.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →