Designing a Highly Reliable Service

A system design interview guide to building a service that stays up when its dependencies, machines, and whole datacenters do not — the toolkit an interviewer expects when the prompt is “design a highly reliable service for X.”

Reliability is not a feature you bolt on at the end; it is a set of deliberate choices made at every layer, from how you define “up” to how a single call times out. A highly reliable service assumes that everything fails — processes crash, disks fill, networks partition, dependencies slow to a crawl — and is engineered so that no single failure, and ideally no single class of failure, takes the whole system down. This guide walks the toolkit: first defining reliability precisely with SLIs, SLOs, and an error budget; then the structural defenses of redundancy across failure domains and health-aware load balancing; then the request-path patterns — timeouts, retries, circuit breakers, bulkheads — that keep a slow dependency from becoming an outage; and finally rollout safety and disaster recovery for when a whole region is lost.

Contents

  1. Requirements & Scope
  2. SLIs, SLOs & the Error Budget
  3. Redundancy & Failure Domains
  4. Health Checks & Load Balancing
  5. The Request-Path Resilience Toolkit
  6. Idempotency & Backpressure
  7. Rollout Safety & Disaster Recovery
  8. Bottlenecks & Tradeoffs
  9. Summary

1. Requirements & Scope

Start by pinning down what reliability means for this specific service, because the answer drives every later choice. Frame it as functional and non-functional requirements.

Interview tip: Do not promise 100% availability — it signals inexperience. Pick a target with the interviewer, translate it into a downtime budget out loud, and let that budget justify how much redundancy and complexity you add. Over-engineering to five nines a service that only needs three is a real design smell.

2. SLIs, SLOs & the Error Budget

You cannot make something reliable that you have not defined and measured. Three linked concepts do this, and getting the vocabulary right is half the battle in an interview.

SLI feeds SLO which yields an error budget, shown as a partly consumed budget bar
The SLI is the measured ratio; the SLO is the target over a window; the error budget is everything the SLO does not require. The budget is a shared currency — spend it on shipping features when healthy, and freeze risky releases when it runs dry.

The error budget is the single most useful idea here because it turns reliability from an argument into an arithmetic. When the budget is healthy, the team can ship features and take risks. When an incident burns through it, the policy is automatic: freeze risky changes and spend engineering on stability until the budget recovers. It aligns product velocity and reliability instead of pitting them against each other.

SLOError budgetDowntime / monthDowntime / year
99% (two nines)1%~7.2 hours~3.65 days
99.9% (three nines)0.1%~43 minutes~8.76 hours
99.99% (four nines)0.01%~4.3 minutes~52.6 minutes
99.999% (five nines)0.001%~26 seconds~5.26 minutes

3. Redundancy & Failure Domains

Structural reliability comes from having no single point of failure. Every component that can fail must have a redundant peer, and — crucially — the peers must fail independently. That independence is captured by the idea of a failure domain: a blast radius that can go down together, such as a process, a host, a rack, an availability zone, or an entire region. You get reliability by spreading redundant copies across domains so that losing one domain loses only a fraction of capacity.

Clients to a global load balancer, fanning out to two regions, each with a regional LB, three replicas across AZs, and a replicated datastore
Stateless service replicas are spread across availability zones within a region, and the whole stack is duplicated across regions. A global load balancer routes away from unhealthy regions; a regional load balancer spreads load across healthy replicas. The datastore keeps a synchronous replica so a machine loss never loses acknowledged data.

The pattern has a few load-bearing choices. Make the service tier stateless so any replica can serve any request and replicas are trivially interchangeable — state lives in the datastore, not in the process. Run N+2 capacity, not N+1, so you survive a failure even while another node is down for maintenance. Spread replicas across availability zones so a single power or network event cannot take them all. And run in more than one region, either active-active (both serving) or active-passive (one hot standby), so that the loss of an entire region degrades rather than destroys the service.

Failure domainExample failureDefense
ProcessCrash, OOM, deadlockSupervisor restarts; multiple replicas.
Host / rackHardware, power, ToR switchSpread replicas across hosts and racks.
Availability zoneZone power / network lossReplicas in ≥3 AZs; quorum survives one.
RegionNatural disaster, fiber cutMulti-region active-active or hot standby.

4. Health Checks & Load Balancing

Redundancy only helps if traffic actually avoids the broken copies, and that is the job of health checks feeding a load balancer. A load balancer continuously probes each backend and routes requests only to those reporting healthy, so a crashed or degraded replica is removed from rotation within seconds rather than serving errors.

The subtlety is in what “healthy” means. A shallow (liveness) check confirms the process is running and answering; a deep (readiness) check confirms it can actually do useful work — its dependencies are reachable, its caches are warm, it is not overloaded. Readiness is what should gate load balancing, but it must be designed carefully: if a deep check marks every replica unhealthy the instant a shared dependency blips, the load balancer will drain all traffic and turn a minor dependency wobble into a total outage. The fix is to fail health checks gradually and to distinguish “I am broken” from “a thing I depend on is briefly slow.”

Interview tip: Call out the health-check failure mode explicitly — a deep check that is too aggressive can cause a correlated, self-inflicted outage where the whole fleet drains at once. Interviewers love that you know redundancy can be defeated by a health check that treats a shared-dependency blip as every replica being dead.

5. The Request-Path Resilience Toolkit

Structural redundancy protects against a replica dying. A different, equally common threat is a dependency that does not die but turns slow — and a slow dependency, without defenses, will exhaust your threads, queue requests, and cascade into a full outage. The request-path toolkit is the set of patterns that contain that.

Six resilience patterns: timeout+deadline, retry with backoff+jitter, circuit breaker, bulkhead, load shedding, graceful degradation
Every outbound call is wrapped in defenses: a timeout bounds how long it can hang, retries with backoff and jitter recover from transient blips without stampeding, a circuit breaker stops calling a failing dependency, bulkheads isolate resource pools, load shedding rejects low-priority work under stress, and graceful degradation serves a reduced result rather than none.

Timeouts & deadlines

Never make an unbounded call. Every request to a dependency gets a timeout, and better still a deadline that propagates down the call chain: if the caller has 100 ms left, the callee must not spend 200 ms. Deadline propagation prevents wasted work on requests whose caller has already given up, and it is the single most effective guard against cascading slowness.

Retries with backoff and jitter

Transient failures — a dropped packet, a brief blip — are worth retrying, but naive retries are dangerous. Retrying immediately, or retrying in lockstep across many clients, produces a retry storm that hammers an already-struggling dependency exactly when it is weakest. The fix is exponential backoff (wait longer after each attempt) plus jitter (randomize the wait so clients do not synchronize). Cap the number of attempts, and only retry idempotent operations.

function call_with_retry(op, deadline):
  attempt = 0
  while attempt < MAX_ATTEMPTS and now() < deadline:
    try:
      return op(timeout = remaining(deadline))   # bound each try
    except Transient:
      base  = BASE_DELAY * (2 ** attempt)         # exponential
      sleep(random(0, base))                      # full jitter
      attempt += 1
  raise Unavailable                               # give up, do not storm

Circuit breakers

When a dependency is clearly down, retrying at all is counterproductive. A circuit breaker tracks the recent error rate to a dependency; once it crosses a threshold the breaker opens and calls fail fast without even attempting the network, giving the dependency room to recover and freeing the caller’s threads. After a cooldown it goes half-open, letting a trickle of requests through to test recovery, and closes again if they succeed. This converts a slow, thread-exhausting failure into an instant, cheap one.

Bulkheads

Named after a ship’s watertight compartments, a bulkhead isolates resources so a failure in one area cannot sink the whole vessel. Give each downstream dependency its own connection pool and thread pool, so that if dependency A hangs and saturates its pool, calls to healthy dependency B still have threads to run on. Without bulkheads, one slow dependency consumes the shared thread pool and stalls every request, related or not.

6. Idempotency & Backpressure

Two more properties make the difference between a service that survives load and one that melts under it.

Idempotency. Retries, at-least-once delivery, and client reconnects all mean the same operation can arrive more than once. An operation is idempotent if applying it twice has the same effect as applying it once. Reads and overwrites are naturally idempotent; increments and appends are not. The standard fix is an idempotency key: the client attaches a unique id to each logical operation, the server records which keys it has already applied, and a duplicate returns the original result instead of doing the work again. This is what lets you retry safely without double-charging a customer or double-writing a record.

Backpressure. A reliable service must protect itself from more load than it can handle rather than accepting everything and collapsing. Backpressure is the family of mechanisms that push load back: bounded queues that reject when full, concurrency limits, and load shedding that drops low-priority requests first when the system is saturated. The counterintuitive but essential move is to reject early and cheaply — a fast rejection preserves capacity for the requests you do accept, whereas accepting everything guarantees that all requests get slow and then all fail. Admission control at the front door is more reliable than unbounded queues behind it.

Interview tip: When asked “what happens under overload?” the strong answer is not “autoscale” (which is too slow for a spike) but “shed load and degrade gracefully.” A service that serves 90% of traffic well and rejects 10% cleanly is far more reliable than one that tries to serve 100% and fails all of it.

7. Rollout Safety & Disaster Recovery

The leading cause of outages is not hardware — it is change. So a reliable service treats deployment as a first-class reliability concern, and prepares for the rare day it loses an entire region.

Safe rollouts

Disaster recovery: RTO & RPO

For catastrophic loss, two numbers define the target. RTO (Recovery Time Objective) is how quickly you must be back up; RPO (Recovery Point Objective) is how much recent data you can afford to lose. They drive the DR strategy directly and trade cost against recovery.

StrategyRTO / RPOCost
Backup & restoreHours / hoursCheapest — restore from backups on demand.
Pilot lightTens of min / minutesCore replicated, rest spun up on failover.
Warm standbyMinutes / secondsScaled-down copy always running, scale up on failover.
Active-activeSeconds / ~zeroMost expensive — both regions serve live.

Whichever you pick, the discipline that matters is testing it: run regular failover drills and game days, because a DR plan that has never been exercised is a hypothesis, not a capability.

8. Bottlenecks & Tradeoffs

Every reliability mechanism has a cost, and naming the tradeoffs is what separates a checklist from a design.

9. Summary

A highly reliable service is built by assuming failure everywhere and layering defenses: define reliability precisely, remove single points of failure, keep traffic away from broken copies, contain slow dependencies on the request path, and rehearse recovery.

ConcernMechanism
How do we define “reliable”?SLIs measured from telemetry, an SLO target, and an error budget that governs release risk.
How do we survive component failure?Stateless N+2 replicas spread across AZs and regions — independent failure domains.
How do we keep traffic off broken replicas?Health-aware load balancing with readiness checks, outlier ejection, and draining.
How do we survive a slow dependency?Timeouts + deadlines, retries with backoff & jitter, circuit breakers, bulkheads.
How do we retry safely?Idempotency keys so duplicate operations apply exactly once.
How do we survive overload?Backpressure, load shedding, and graceful degradation — reject early, serve partial.
How do we deploy without outages?Canary + progressive rollout with automated health gates and rollback; feature flags.
How do we survive losing a region?DR strategy sized to RTO/RPO, tested with regular failover drills.
The recurring theme: assume everything fails, and make each failure local, cheap, and recoverable. Redundancy handles the parts that die; timeouts, breakers, and bulkheads handle the parts that merely slow down; error budgets and safe rollouts keep humans from becoming the outage. Reliability is the sum of many small, deliberate defenses, sized to a target you stated up front.