QuickCart Stage 9: Surviving Failure Without Losing Orders
Make QuickCart survive slow dependencies, lost zones and stale leaders with timeouts, breakers, degradation, failover, Raft, fencing tokens and chaos tests.
What you'll learn: By the end of this stage you will be able to design QuickCart so that a slow recommendation service, a failing payment provider, a lost availability zone or a paused scheduler costs a feature or a few seconds, not an order. You will set timeouts and retry budgets, place circuit breakers and bulkheads, rank what degrades first, define health checks, choose a failover and disaster-recovery level, and explain consensus, locks, clocks, gossip, idempotency and chaos experiments.
A ship stays afloat not because the sea is calm but because a hole floods one compartment instead of the whole hull. The earlier stages made QuickCart faster and bigger. This one makes it hold together when parts break, because at this size parts always break.
It continues from Stage 8, where the monolith became services. More services means more network calls, and every call can fail. The level lessons linked under Related reading go deeper; here each idea is explained and then applied to one system.
Where QuickCart is now
After Stage 8, QuickCart is a set of services behind an API gateway: Catalogue, Cart and Checkout, Orders, Payments, Inventory, Delivery and Notifications. Checkout is asynchronous (Stage 5) and payments use idempotency keys (Stage 6). Every number below is an illustrative assumption from the course's shared estimates.
Browsing peaks at about 2,300 requests per second and checkout at about 58 per second (Stage 1).
Suppose the SLO is 99.9 percent checkout success per 30-day month. The error budget is 0.1 percent of 43,200 minutes, about 43 minutes.
Three things now threaten that budget.
Slow, not dead. Recommendations is optional, yet the cart page calls it on every view. When its feature store slows to 20 seconds per call, cart threads wait, the shared pool fills, and checkout stops although Payments and Inventory are healthy.
Retries that multiply. The gateway, Checkout and Payments each retry three times, which is four attempts per layer. One checkout can become 4 x 4 x 4 = 64 provider calls, so 58 checkouts per second can become 58 x 64 = 3,712 calls per second to a provider that is only struggling.
Single points. One availability zone (AZ, an isolated data-centre location inside a region) holds the Orders primary database, the courier scheduler and the metadata store.
Figure 1. Before: one slow optional dependency sinks checkout
How to read this: the red box is slow, not down. Cart and Checkout waits on the Recommender (the Recommendations service) with the same threads it needs for payment and stock calls, so the amber boxes stop working while Payments and Inventory stay healthy.
The core idea
Resilience is deciding in advance what fails, how far the failure spreads, and what the shopper sees. Each pattern answers one question.
Bound it: how long do we wait and how often do we try? (timeouts, retries)
Contain it: how does one failure avoid using up shared threads and capacity? (breakers, bulkheads, degradation)
Replace it: what takes over when a machine, zone or region is gone, and how much data can we lose? (health checks, failover, disaster recovery)
Agree and repair: what stays correct when copies disagree or messages repeat? (consensus, locks, clocks, gossip, idempotency)
Chaos engineering then checks that all four work. QuickCart ranks its features: the order path is sacred and everything else may be dropped.
Calls that fail slowly
Timeouts
A timeout is the longest a caller waits for an answer. With none, a call can wait forever, and "forever" times the request rate is an exhausted thread pool. A timeout is a decision about how long the answer is still useful. Set both a connect timeout (opening the connection) and a request timeout (waiting for the reply).
To choose a value, the Amazon Builders' Library suggests picking an acceptable false-timeout rate (its example is 0.1 percent) and using the matching percentile of the downstream's measured latency, here p99.9.
Timeouts must also nest. The shopper's request has a deadline, and each call passes down the time remaining, so an inner timeout is always shorter than the outer one. If "place order" must answer in 4 seconds (illustrative), the provider call inside it must time out in under 3. A 30-second provider timeout inside a 4-second request is work nobody waits for.
Retries, exponential backoff and jitter
A retry repeats a failed call. It helps with brief faults such as a dropped packet and hurts under overload, because it adds load to a service already struggling. Four rules keep retries safe.
Retry only what is safe to repeat. Reads are safe. RFC 9110 (section 9.2.2) defines PUT, DELETE and the safe methods (GET, HEAD) as idempotent, meaning a repeat leaves the same state; POST is not. "Charge the card" is safe to repeat only with an idempotency key, a client-chosen token that lets the server return the first result for a repeat (Stage 6).
Retry only retryable errors. A timeout, reset or 503 may pass; a 400 never will.
Back off exponentially with jitter.Exponential backoff doubles the wait after each failure. Jitter randomises it so thousands of clients do not retry together. The "full jitter" variant from the AWS Architecture Blog waits a random time between zero and the current ceiling.
Retry at one layer, with a budget. The Google SRE book shows three retries at each of three layers giving up to 4^3 = 64 attempts, and recommends a retry budget (its example: 60 retries per minute per process) and randomised exponential backoff. When the budget is spent, stop retrying, because that is when failures are widest.
// Full jitter: wait a random time between 0 and a ceiling that doubles per attempt.functionbackoffMs(attempt: number, baseMs = 100, capMs = 5000): number {
const ceiling = Math.min(capMs, baseMs * 2 ** attempt);
returnMath.random() * ceiling;
}
// attempt 0 waits up to 100 ms, attempt 3 up to 800 ms, attempt 10 is capped at 5000 ms.
The 100 ms base and 5 s cap are teaching values. QuickCart's rule (illustrative): only Payments retries provider calls, at most twice, only with an idempotency key and only while retries stay under 10 percent of calls. The gateway and Checkout do not retry. The worst case drops from 64 times normal load to about 1.1 times.
Figure 2. One payment call with a timeout, one jittered retry and the same key
sequenceDiagram
accTitle: A payment call with timeout and retry
accDescr: Payments calls the provider with an idempotency key, the first attempt times out, Payments waits a random time and retries with the same key, and the provider answers once.
participant C as Checkout
participant P as Payments
participant X as Provider
C->>P: authorise order 8812 with deadline 4000 ms
P->>X: charge with key K, timeout 1500 ms
X--xP: no reply in time
Note over P: wait random 0 to 200 ms, retry budget allows it
P->>X: charge with the same key K
X-->>P: approved, one charge only
P-->>C: approved
How to read this: the first attempt's outcome is unknown, because the provider may have charged before the reply was lost. Reusing key K makes the second attempt safe; without it a retry could charge the shopper twice.
Containing the damage
Circuit breaker
A circuit breaker wraps calls to a dependency and watches the results. As Martin Fowler describes it, it has three states.
Closed: calls pass through and failures are counted.
Open: failures crossed the threshold, so calls fail at once without touching the dependency. Threads are freed and the dependency can recover.
Half-open: after a wait, a few trial calls go through. Success closes the breaker; a failure opens it again.
Figure 3. The breaker state machine
stateDiagram-v2
accTitle: Circuit breaker states
accDescr: A breaker is closed until the failure rate crosses a threshold, then open for a wait period, then half-open for trial calls, which close or reopen it.
[*] --> Closed
Closed --> Open: failure or slow-call rate over threshold
Open --> HalfOpen: wait period over
HalfOpen --> Closed: trial calls succeed
HalfOpen --> Open: a trial call fails
How to read this: the breaker moves only on measured outcomes. Open is the protective state: it trades a slow failure for a fast one.
Thresholds set its behaviour. An illustrative setup: open when, in the last 10 seconds, at least 20 calls were made and at least 50 percent failed or exceeded the timeout. The minimum call count stops one failure on a quiet minute from tripping it. Count slow calls as failures, since a 20-second success is not healthy, and keep one breaker per dependency, never a global one.
Worked example: the Recommendations breaker sees 20 calls in 10 seconds and 12 time out. 12 of 20 is 60 percent, so it opens for 30 seconds and the cart page shows a popular-items list without spending threads. Then 3 trial calls are allowed; if they succeed it closes.
What the shopper sees while it is open is the fallback: cached data, a default, an empty list, a queued action or a clear error. Stale recommendations are harmless; a stale price at payment is not.
Bulkhead
A bulkhead gives each dependency a fixed share of a scarce resource, such as its own thread or connection pool, so one cannot take it all.
Little's law shows why: calls in flight equal arrival rate times latency. With 58 checkouts per second and a 0.4 s payment call (illustrative), about 23 calls are in flight. If the provider slows to 8 s, the same traffic wants 58 x 8 = 464. With one shared pool of 200 threads, payments alone drain it. With a payments pool of 50, the 51st concurrent call is rejected at once and Inventory and Catalogue keep their threads. The cost is idle capacity in normal times, an insurance premium.
Graceful degradation
Graceful degradation offers a simpler service instead of none, using a ranking decided in advance. QuickCart drops in this order.
Order
Feature
Degraded mode
Why it is safe
1
Recommendations
Static "popular near you" list
No order depends on it
2
Search ranking
Plain text match
Shoppers still find products
3
Notifications
Queue and deliver late
The order still happens
4
Live courier map
Last known location with its age
Status stays correct
Never
Checkout and payment
Accept and queue if downstream is slow
This is the business
Checkout degrades only at its edges: loyalty points and receipts move to the asynchronous pipeline.
Figure 4. What to shed, and when
flowchart TD
accTitle: Degradation ladder for QuickCart
accDescr: A decision flow that sheds recommendations first, then search ranking, then delays notifications, and never sheds checkout.
S["Dependency slow or load above limit"] --> R{"Recommendations breaker open or load high?"}
R -->|yes| R1["Serve static popular list"]
R -->|no| K["Normal service"]
R1 --> Q{"Still over limit?"}
Q -->|yes| Q1["Use plain text search ranking"]
Q -->|no| K
Q1 --> N{"Still over limit?"}
N -->|yes| N1["Queue notifications and delay them"]
N -->|no| K
N1 --> C["Protect checkout, reject lower-priority requests with 503"]
How to read this: each step sheds less valuable work first, and the last step reserves what remains for checkout. Each step is a switch an engineer or an automatic rule can flip.
The SRE book notes degraded modes rarely run, so exercise them on purpose.
Knowing what is healthy
Health checks: liveness, readiness and startup
A health check is a small request asking an instance whether it is fit for a purpose. The Kubernetes documentation separates three questions (Stage 10 covers Kubernetes itself).
Check
Question
When it fails
Liveness
Is the process stuck beyond recovery, such as a deadlock?
The kubelet restarts the container
Readiness
Can it take traffic now?
The pod leaves the Service endpoints; no restart
Startup
Has it finished starting?
The container is killed and restarted; liveness and readiness wait until it passes
The classic mistake is a liveness probe that depends on other services. The documentation warns that a wrongly built liveness probe can cause cascading failures. If Payments' liveness check called the database and the database slowed, every Payments pod would restart at once. Keep liveness about the process. Put a dependency in readiness only if the pod cannot serve without it, and remember that if every pod shares one database, a blip removes them all together.
startupProbe:httpGet: { path:/healthz/started, port:8080 }
periodSeconds:5failureThreshold:30# allows 5 x 30 = 150 s to start (illustrative)livenessProbe:httpGet: { path:/healthz/live, port:8080 }
periodSeconds:10failureThreshold:3readinessProbe:httpGet: { path:/healthz/ready, port:8080 }
periodSeconds:5failureThreshold:2
The paths are QuickCart's own names. Load balancers do the same job at the edge by probing targets (load balancing).
When a machine, a zone or a region goes away
Redundancy and failover
Redundancy means more than one of everything that can fail; failover is the switch to the replacement. Reason in failure domains: process, machine, AZ, region.
Failure domain
QuickCart answer
Cost
Process or machine
Many stateless replicas on different machines
Spare capacity
AZ
Services, caches and brokers in 2 or 3 AZs; Orders database with a synchronous standby in another AZ
Cross-AZ charges; slower commits
Region
A second region (next section)
Most money and complexity
State needs more care than servers. With synchronous replication a commit is acknowledged only after the standby has it, so losing the primary's zone loses no committed order, at the price of write latency; asynchronous replication can lag (replication). Failover can also be wrong: the AWS disaster-recovery paper warns that automatic region failover "should be used with caution", because a false alarm itself causes loss, and many teams automate every step but keep a human pressing the button. Inside a region, automatic promotion is normal, but it needs agreement on who the new primary is, which is consensus.
Disaster recovery: RPO and RTO
Disaster recovery (DR) is surviving the loss of a whole site. RPO (recovery point objective) is how much recent data you can lose, as time. RTO (recovery time objective) is how long you can be down. Tighter targets cost more. The AWS whitepaper lists four patterns:
Pattern
Runs in region B
Recovery
Cost
Backup and restore
Backups only
Slowest
Lowest
Pilot light
Data replicated, core services off
Switch on, scale
Low
Warm standby
Scaled-down copy running
Scale up
Medium
Multi-site active-active
Full copy serving traffic
Near zero for most disasters
Highest
In active-passive, region B waits; in active-active, both serve, so writes from both sides need a rule: route each shopper's writes to one home region (Orders is already sharded by shopper, Stage 4) or accept last-writer-wins and its loss risk. Replication also copies corruption, so keep point-in-time backups.
QuickCart's targets, all illustrative:
Data
RPO
RTO
Pattern
Orders and ledger
Zero for an AZ loss; under 1 minute for a region loss
30 minutes
Sync standby, async replica in B, warm standby
Catalogue, search index
1 hour (rebuildable)
2 hours
Pilot light
Courier locations
Minutes (transient)
15 minutes
Re-ingested from couriers' apps
Analytics warehouse
24 hours
1 day
Backup and restore
Set targets per data type, and test the plan.
Agreeing in a cluster
Consensus and leader election (Raft)
Consensus is a group of nodes agreeing on a value or an order of values despite crashes and delays; leader election is agreeing who is in charge. QuickCart needs it for a small metadata store: the shard map from Stage 4, configuration, the flags that switch degradation on, and the lease naming which scheduler instance assigns couriers.
Raft, from Ongaro and Ousterhout's 2014 paper "In Search of an Understandable Consensus Algorithm", works like this at concept level.
Roles and terms. Nodes are followers, candidates or a leader. Time is split into numbered terms, each with at most one leader. Messages carry the sender's term, and a node that sees a higher term steps down, which is how a stale leader is recognised.
Election. A follower that hears no heartbeat for a randomised timeout becomes a candidate, raises the term, votes for itself and asks for votes. Each node gives one vote per term, only to a candidate whose log is at least as up to date as its own. A majority wins.
Log replication. The leader appends each command to its log and sends it out. Once a majority has stored it, it is committed.
Take QuickCart's three-node metadata store, one node per AZ. A majority is 2 of 3, so it survives one loss; five nodes survive two. If the leader's AZ is cut off, the other two elect a leader in a higher term and keep committing. The isolated node cannot reach a majority, so it cannot commit writes: Raft chooses consistency over availability in a partition (CAP).
The cost is that every write needs a majority round trip, so the store is small and slow compared with a database. Use it for coordination, never for orders, and run a proven system (etcd, ZooKeeper, a managed service) rather than your own. See Consensus and Leader Election.
Distributed locks and fencing tokens
A distributed lock lets one process at a time do something, such as assign courier 17 to an order. The tempting recipe is a Redis lock: SET resource_name my_random_value NX PX 30000 (set if absent, expire after 30 seconds), released only if the stored value is still yours. The Redis documentation names the limits:
Expiry. If the holder pauses (garbage collection, a stalled VM) past the TTL, a second client takes the lock and the first wakes still believing it holds it.
Failover. Redis replication is asynchronous, so a promoted replica may not have the key and another client can take the lock.
Clocks. Redis does not use a monotonic clock for expiry, so a wall-clock jump can shorten a lock.
Martin Kleppmann's analysis adds the central point: a lock for efficiency (avoiding duplicate work) can tolerate these gaps; a lock for correctness needs a fencing token, a number that rises with every grant. Each write carries it, and the resource keeps the highest token seen and rejects lower ones.
Figure 5. A paused scheduler is stopped by the fencing token
sequenceDiagram
accTitle: Fencing token on courier assignment
accDescr: Scheduler A gets token 33 and pauses, its lease expires, scheduler B gets token 34 and assigns the courier, and A's late write with token 33 is rejected.
participant A as Scheduler A
participant L as Lock service
participant B as Scheduler B
participant D as Assignments table
A->>L: acquire lease for zone 7
L-->>A: granted, token 33
Note over A: long pause, lease expires
B->>L: acquire lease for zone 7
L-->>B: granted, token 34
B->>D: assign courier 17, token 34
D-->>B: accepted
A->>D: assign courier 21, token 33
D-->>A: rejected, stale token
How to read this: A did nothing wrong in its own view; it slept past its lease. The protection lives in the table A writes to, not in A.
In SQL the check is a conditional update (fence starts at 0), and zero rows changed means rejected:
Prefer avoiding locks. Most "exclusive" actions are a conditional write. Reserving stock needs no lock if the database decides atomically:
-- Stage 3 inventory table, with a reserved column addedUPDATE inventory
SET reserved = reserved + $3WHERE store_id = $1AND product_id = $2AND stock - reserved >= $3;
One row changed means reserved; none means out of stock. Reach for a lock service only when no atomic check exists.
Clocks and ordering
Machines do not share a clock. Each physical clock drifts, and NTP corrects it only to limited accuracy, so machines disagree by some skew. Suppose Inventory (clock fast) writes "mango stock = 0" stamped 10:00:05, and a store's app (clock slow) later writes "mango stock = 40" stamped 10:00:03. A last-write-wins store keeps 0 and silently loses the newer write. Use monotonic clocks for durations, and never compare timestamps from two machines to decide what came first.
What orders events instead is causality: A is before B if A could have influenced B.
A Lamport clock is a counter per process: increment per event, attach it to messages, and on receipt take the larger of local and received, plus one. If A happened before B, A's stamp is smaller; the reverse is not guaranteed.
A vector clock keeps one counter per process. Comparing two vectors shows "before" or concurrent (neither knew of the other), so a store can keep two conflicting versions for merging instead of dropping one. Vectors grow with the number of writers.
A hybrid logical clock (HLC) pairs a physical time with a small counter and never moves backwards. If B's wall clock reads 90 and it receives a message stamped (100, 0), B's clock becomes (100, 1) and its next event is (100, 2), though its wall clock still says 90. It stays near real time and respects causality, which suits QuickCart's order-event timestamps (real systems add drift bounds).
So QuickCart updates carry versions or a status rank, not bare timestamps. See IDs, Clocks and Ordering.
Gossip
Gossip shares state across a large cluster without a coordinator. Each interval, a node exchanges what it knows with a random peer, which passes it on, so news spreads like a rumour in roughly logarithmic rounds of cluster size (a classic result from the epidemic-algorithms literature).
Apache Cassandra's documentation shows it: each node gossips every second with a random peer (and sometimes a seed node), tagging state with a (generation, version) pair so old information is ignored. Failure detection is local: each node runs a variant of the Phi Accrual Failure Detector on heartbeats, and "down" is that node's own judgement, not a broadcast. The SWIM paper (Das, Gupta and Motivala, 2002) describes a related design that pings a member directly and, if that fails, asks a few others to ping it before declaring it failed.
Gossip suits membership and failure detection in big clusters, such as a courier-location store. It is eventually consistent, so do not use it where one answer is needed now, such as who the leader is.
Making repeated and reordered messages harmless
Idempotent and commutative operations
Retries, redelivered events and replicas merging in different orders all deliver messages twice or out of order. Two properties neutralise that. An operation is idempotent if doing it twice equals doing it once, and commutative if order does not matter.
Operation
Idempotent
Commutative
Safe to retry and merge?
Set status to "paid"
Yes
No
Retry yes; merging needs a rule
stock = stock - 1
No
Yes
Retry no
Set union, or keep the maximum
Yes
Yes
Both
Give order statuses a rank (placed, paid, picked, delivered) and apply "take the higher rank". A duplicate or late "paid" event then cannot move a delivered order backwards. Cancellation needs its own explicit rule.
A G-counter (grow-only counter) keeps one count per node and merges by taking the maximum per node. If node A holds {A: 3, B: 0} and node B holds {A: 2, B: 4}, the merge is {A: 3, B: 4}, value 7, however often or in whatever order merged. CRDTs (conflict-free replicated data types; Shapiro and others, 2011) generalise this. Where commutativity is impossible, use an idempotency key and a deduplication table (Stage 6).
Proving it works
Chaos engineering
Settings are guesses until tested. Principlesofchaos.org defines chaos engineering as experimenting on a system to build confidence that it withstands turbulent conditions. An experiment has five parts.
Steady state: a measurable output, such as checkout success and p99 latency.
Injected event: added latency, a killed pod, a blocked path.
Disproof attempt: compare against a control.
Blast radius: start with one pod or 1 percent of traffic, with a kill switch and abort conditions.
A game day is a scheduled rehearsal that includes people: detection, paging, communication, rollback. Often the runbook or alert is what breaks. QuickCart starts in staging, lowest risk first.
A realistic scenario, not a case history. This is a composite educational reference design. It does not describe any real company, and every number is an illustrative assumption.
The situation
On a Saturday at 19:00 the Recommender's feature store slows after a data refresh. The cart page keeps calling it, Cart and Checkout runs out of threads, the gateway retries, and checkout success falls to about 80 percent for 25 minutes. That is 20 percent failures for 25 minutes, about 5 of the month's 43 error-budget minutes. Two weeks later a payment-provider brownout makes retries stack across three layers. The brief: survive both, plus an AZ loss, within a quarter.
Starting architecture
Figure 6. Starting architecture: shared pools, retries in three places, single points in one AZ
How to read this: amber marks a risk the incident exposed: retries at several layers, a shared pool, and single instances in one zone.
Options considered
Option
What it fixes
What it costs
Risk
Lower timeouts only
Shorter thread holds
One config line
Pool still shared; retries still multiply
Containment package (timeouts, one retry layer, breakers, bulkheads, ladder, probes)
Optional features cannot sink checkout
Weeks of work, dashboards
Bad thresholds cause false trips
Add multi-AZ and a warm-standby region
AZ and region loss
Standing cost, drills
Untested failover is fiction
Active-active now
Near-zero RTO
Conflict design, top cost
Not required by the targets
The decision
Field
Entry
Context
An optional dependency sank checkout, retries multiplied load, and single points sit in one AZ.
Decision
Containment package, multi-AZ, and a warm-standby region at the targets above. Recommender gets its own pool, a 150 ms timeout and a static fallback. Payments owns provider retries (key, 10 percent budget). A three-node metadata store across AZs issues the scheduler lease and fencing tokens.
Consequences, good
A recommendation outage becomes a missing widget; an AZ loss loses no committed order.
Consequences, bad
More configuration, idle capacity, a standing second region, drills to maintain.
Revisit when
Region drills exceed the 30-minute RTO, or the business needs near-zero cross-region downtime.
Rollout and verification
Run breakers in shadow mode (log only) for a week.
Canary at 5 percent behind a flag, then widen.
Move retries last: enable the Payments budget before disabling gateway retries, or there is a moment with none.
Run the experiments above in staging, then a production game day for AZ-1.
Dashboards: checkout success and p99, breaker state and pool use per dependency, retry ratio (target under 10 percent), shed requests by priority, metadata-store leader changes, and replica lag.
The 2 a.m. failure
At 02:00 AZ-1 loses its network. The page reads "Orders primary unreachable, checkout success 40 percent". The scheduler in AZ-1 is partitioned, not dead.
Figure 7. AZ-1 is lost and the database fails over
sequenceDiagram
accTitle: Failover after losing an availability zone
accDescr: Probes detect that the primary in AZ-1 is unreachable, the standby in AZ-2 is promoted, checkout resumes, and the old scheduler's stale writes are rejected.
participant H as Health checks
participant P as Primary AZ-1
participant S as Standby AZ-2
participant C as Checkout
participant O as Old scheduler
H->>P: probe
P--xH: no answer
H->>S: promote after failure threshold
S-->>H: now primary
C->>S: orders write
S-->>C: committed
O->>S: assign courier, token 33
S-->>O: rejected, token 34 is current
How to read this: the standby already held every committed order because commits waited for it. The old scheduler wakes later with a stale token and does no damage.
First three checks: the checkout and breaker dashboard (which dependency tripped); promotion status and replica lag (did the standby take over, and was it in sync); the metadata store's leader and term (a new leader in AZ-2 or AZ-3?). Mitigation: let promotion finish, confirm the new primary accepts writes, and shed low-priority traffic while two AZs carry the load. Follow-ups: alert on stale-token rejects and test that two AZs can carry peak load. This is a realistic scenario, not a case history.
Architecture after this stage
Figure 8. QuickCart after Stage 9: what this stage added
How to read this: accent boxes are new or changed in this stage. Normal traffic follows solid arrows through Region A; the red dashed arrows are fallback paths that carry traffic only when something has failed.
What QuickCart looks like now
Component
Why it exists
What it costs
Timeouts and deadlines
Bound waiting
Tuning from measured latency
Retry budget, jitter, idempotency keys
Safe repeats, no storms
Keys and dedup storage
Breakers and bulkheads
Contain slow dependencies
Idle capacity, threshold upkeep
Degradation ladder and flags
Keep checkout alive
Fallback paths to build and test
Probes
Restart or remove bad pods
Careful endpoint design
Multi-AZ, sync standby
Survive an AZ loss
Slower commits, cross-AZ traffic
Warm standby region
Survive a region loss
Standing cost, drills
Metadata store, fencing tokens
Safe leader, config, assignment
A cluster to run
Chaos experiments
Prove the above
Engineer time, risk management
Architect's notes
Draw the dependency graph first. List every call on the critical path, mark it critical or optional and record its real p99. Optional calls often sit on the critical path.
Set RPO and RTO per data type with the business. One number for everything is too costly or too weak.
Unknown outcome is its own state. A timed-out charge may have succeeded. Model "pending" and reconcile; never map a timeout to "failed".
Defer active-active. Add it when a measured requirement demands it; containment and tested failover come first.
Common mistakes
No timeout, or a huge default. Threads pile up behind a slow dependency.
Retrying at every layer. Three layers of three retries is 64 attempts.
Retrying non-idempotent calls. Double charges.
A breaker with no safe fallback. It only fails faster.
A liveness probe that checks the database. A blip restarts every pod.
A lock without a fencing token. A paused holder corrupts data.
Trusting timestamps across machines. Skew silently drops writes.
Untested failover. A plan that never ran is a guess.
Key takeaways
Slow is worse than down. A slow dependency drains callers' threads and spreads failure.
Bound every wait. Timeouts nest inside a deadline and come from measured percentiles.
Retries are load. Use idempotency, jitter, one retry layer and a budget.
Contain, then degrade. Breakers and bulkheads limit spread; a ranked ladder decides what to drop, and checkout is never on it.
Three probes, three meanings. Liveness restarts, readiness removes from traffic, startup protects slow starts.
RPO and RTO are priced business choices. Set them per data type and test the plan.
Consensus is for small coordination data. Majorities make leaders safe; partitions favour consistency.
Fence your locks, or avoid them. Prefer atomic conditional writes.
Do not trust wall clocks for order. Use versions, logical or hybrid clocks.
Make messages idempotent or commutative. Then retries and merges are safe, and chaos experiments show it.
Practice exercises
Exercise 1: Retry arithmetic (30 min). The gateway, Checkout and Payments each allow 2 retries (3 attempts). How many provider calls does one checkout cause when the provider is down, and how many per second at 58 checkouts per second? Redesign so the worst case is 3 attempts and say which layer keeps its retries.
Exercise 2: Size a bulkhead (45 min). Use Little's law. The cart service takes 2,300 requests per second and 1 in 10 calls the Recommender at 80 ms. How many calls are in flight normally, and at 10 s? Choose a pool size, a timeout and a shopper-visible fallback.
Exercise 3: Targets (1 hour). Write an RPO, RTO and DR pattern for six QuickCart data types, including the notification queue. Justify each number from the business.
Exercise 4: Game day (half day). Plan a game day that takes AZ-1 offline: steady-state metric, hypothesis, abort conditions, who is on call, and a review template with expected, observed and action columns.
Lesson 9 of 11 · Backend System Design Project: Build QuickCart
Make QuickCart survive slow dependencies, lost zones and stale leaders with timeouts, breakers, degradation, failover, Raft, fencing tokens and chaos tests.
In the rest of this lesson
Knowing what is healthy
When a machine, a zone or a region goes away
Agreeing in a cluster
Making repeated and reordered messages harmless
Proving it works
The failure-mode table
Enrolling is free. It unlocks every lesson in Backend System Design Project: Build QuickCart, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.
Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.
Get new articles by email
Free. We email a confirmation link first, and you can unsubscribe from any email.