Skip to the chapter
←System DesignGGroundworkSDSystem Design
System Design/Intermediate/I4
11 / 36 written chapters
/

The cover
  1. B1The mental model
  2. B2Client-server basics
  3. B3Vertical vs horizontal
  4. B4Databases in design
  5. B5Caching fundamentals
  6. B6APIs & communication
  7. B7Walkthrough: URL shortener
  1. I1Load balancing in depth
  2. I2CDNs
  3. I3Message queues & async
  4. I4Consistency models
  5. I5Replication
  6. I6Rate limiting & throttling
  7. I7Designing for availability
  8. I8Storage systems
  9. I9Search systems
  10. I10Microservices
  11. I11Security basics
  12. I12Walkthrough: feed/chat
  13. I13Walkthrough: notifications
  1. A1CAP theorem in depth
  2. A2Transactions and isolation
  3. A3Storage engines
  4. A4Sharding at scale
  5. A5Time, ordering and IDs
  6. A6Distributed consensus
  7. A7Batch & stream processing
  8. A8Fault tolerance
  9. A9Observability at scale
  10. A10Release safety
  11. A11Capacity estimation
  12. A12Case studies
  13. A13Walkthrough: key-value store
  14. A14Walkthrough: web crawler
  15. A15Walkthrough: payments
  16. A16Tradeoff thinking

SDI4 · Part 2 · Intermediate

Consistency models

Every consistency model is just a rule about which stale reads you are willing to let a user see.

17 min read3 diagrams7 sections0 exercises
11 of 36

In this chapter

  • Consistency is a contract about what a read may return
  • Replication lag is the concrete thing underneath all of it
  • The two poles, and the useful middle
  • Quorums: R + W N, with the numbers worked out
  • Linearizability vs serializability — telling them apart for good

and 2 more

about 17 min
07On this page
  1. Consistency is a contract about what a read may return
  2. Replication lag is the concrete thing underneath all of it
  3. The two poles, and the useful middle
  4. Quorums: R + W N, with the numbers worked out
  5. Linearizability vs serializability — telling them apart for good
  6. Choosing per operation: model, cost, and what it looks like to a user
  7. Recognizing it in an unseen problem

Consistency is a contract about what a read may return

Once data exists in more than one place — a replica, a cache, a second region — "what is the current value?" stops having a single answer. A consistency model is the contract you offer the application about which of the possible answers a read is allowed to produce. Stronger models forbid more answers, which costs coordination, which costs latency and availability.

Interviewers push on this because it is the one topic where candidates reliably say something confidently wrong ("we'll use eventual consistency for speed") without noticing they just permitted a user to see their own comment disappear. The senior move is to pick a model per operation and justify it in product terms.

client Alice primary balance = 120 (v2) read replica balance = 100 (v1) t=0 write v2, committed t=2 ms read → 100 replication lag 40 ms Alice deposited 20 and her deposit is not there. Nothing is broken — the system is behaving exactly as specified.
Every consistency anomaly you will ever discuss is a variation on this picture. The models differ only in which arrow they forbid.

Replication lag is the concrete thing underneath all of it

"Eventual consistency" sounds abstract until you attach numbers to it. Lag is the wall-clock delay between a write committing on the primary and being visible on a given replica, and its distribution is wildly skewed — the median is boring and the tail is where your bugs live.

SetupTypical lagTail behaviour
Same-AZ async replica0.5-2 msTens of ms under write bursts
Cross-AZ, same region2-10 ms100 ms+ during compaction or vacuum
Cross-region (us-east → eu-west)Roughly 50-150 ms; the floor is the one-way delay of about 35-45 ms (half of a 70-90 ms round trip), plus apply timeSeconds if the link saturates
Any replica during a bulk write, index build, or long transactionseconds to minutesSingle-threaded replay (classic MySQL) can fall hours behind and never catch up until write traffic drops
Read-through cache with 60 s TTLup to 60 sThis is replication lag too. People forget caches are replicas

The operationally important property: lag is not bounded. Any design that says "the replica is only a few milliseconds behind, so it's fine" is a design that breaks during the exact incident where correctness matters most. If you need a bound, you must enforce one — by reading the primary, by waiting for a version token, or by refusing to serve from a replica whose lag exceeds a threshold.

The two poles, and the useful middle

Strong consistency (informally: every read sees the latest committed write) requires the read to be coordinated with the write. The usual ways are to send the read through the leader after it has confirmed it is still the leader (Raft's ReadIndex, or a clock-based leader lease), or, in a leaderless register, to read a quorum and write the newest value back to a quorum before returning (the ABD algorithm). A plain "read R replicas and take the newest" is not enough, as the warning below shows. Cost: at least one extra round trip, and unavailability whenever the leader or a quorum is unreachable. Eventual consistency promises only that if writes stop, replicas converge. Cost: nothing; guarantee: nearly nothing. Any stale value is legal, in any order, on any read.

Between them sit the session guarantees — the models that actually ship in real products, because they fix the anomalies users can perceive without paying for global coordination.

ModelForbidsHow it's implemented
Read-your-writesYou post a comment and it isn't there on refreshRoute a user's reads to the primary for N seconds after their write; or return a version token (Postgres LSN, MySQL GTID) with the write and have the read wait for a replica that has caught up to it
Monotonic readsRefreshing shows a comment, then it vanishes, then it's backSticky routing: hash the user to one replica so they never move backwards in the replication stream
Consistent prefixSeeing the reply "because he's late" before the question "why?"Preserve write order per partition; don't shard causally-related rows across independently-replicated partitions
Monotonic writesYour second write being applied before your first (set a name, then fix a typo, and the typo wins)Apply one client's writes in the order issued: per-session sequence numbers, or route a session's writes through one leader or one partition log
Writes-follow-readsReplying to a post and having the reply replicate somewhere the post has not reached yetCarry the version you read (token or dependency) on the write, and apply the write only where that version is already present
Causal consistencyAny effect visible before its cause, across usersTrack happens-before with vector clocks or dependency metadata and delay applying an update until its dependencies have landed
Say it like this → "I'll serve reads from replicas, but pin a user to the primary for about 500 ms after any write of theirs — that buys read-your-writes for the person who cares, while everyone else still gets the cheap replica read. If the lag distribution makes a fixed window unreliable, I'll upgrade to returning the write's LSN and having the replica wait for it."
⚠ Read-your-writes breaks on a second device, and on the client's own cache Sticky-to-primary usually keys on the session or connection. Alice updates her profile on her phone, opens her laptop, and the laptop's session hits a lagging replica — the anomaly is back. If cross-device read-your-writes matters, the sticky key has to be the user, not the session, and the version token has to travel with the user (stored server-side or in a cookie), not with the connection.

Quorums: R + W > N, with the numbers worked out

In a leaderless or quorum-replicated store (Dynamo, Cassandra, Riak) you choose three numbers: N replicas hold each key, a write must be acknowledged by W of them, a read must gather R of them. If R + W > N, the read set and the write set must overlap in at least one node by pigeonhole — so at least one responding replica has the newest write, and version stamps let the reader pick it.

writer reader replica 1 written replica 2 written + read replica 3 read only W = 2 R = 2 R + W = 4 > N = 3 the overlap is replica 2 — it must be in both sets so the reader always sees the newest version stamp
The guarantee comes from arithmetic, not from timing: with R + W > N the sets cannot be disjoint, so a reader physically cannot miss the newest committed write.
N, W, ROverlap?Write toleranceRead toleranceCharacter
3, 2, 2Yes (4 > 3)1 node down1 node downThe default. Balanced, survives one failure on both paths
3, 3, 1Yes (4 > 3)0 — any node down blocks writes2 nodes downRead-optimised: 1-replica reads are fast and local, writes are brittle
3, 1, 3Yes (4 > 3)2 nodes down0Write-optimised: fast durable-ish writes, fragile reads
3, 1, 1No (2 < 3)2 down2 downPure eventual consistency. Lowest latency, highest availability, a read can miss a write entirely
5, 3, 3Yes (6 > 5)2 down2 downSurvives two failures on both paths; ~1.5x the write cost. Standard for critical data

Latency follows directly: a W=2 write waits for the second-fastest of three replicas, so it inherits that node's p95, not the fastest node's. Raising W or R moves you further into the tail of the slowest responder — which is why quorum systems tune these per-query rather than globally.

Dry run: N=3, W=2, R=2 — and what breaks it

TimeOperationNodes contactedResulting stateRead/write sets overlap?Result
t0write v1acks from R1, R2 (W=2)R1=v1, R2=v1, R3=v0—Write succeeds
t1readcontacts R2, R3 (R=2)sees v1 (R2), v0 (R3)Yes — R2 is in both the write set {R1,R2} and the read set {R2,R3}Returns v1 — correct
t2write v2acks from R1, R3 (W=2)R1=v2, R2=v1, R3=v2—Write succeeds
t3readcontacts R1, R2 (R=2)sees v2 (R1), v1 (R2)Yes — R1 is in both {R1,R3} and {R1,R2}Returns v2 — correct
t4write v3, during a partition — R1 and R2 unreachable (a Riak or original-Dynamo style store)sloppy quorum acks from R3 (real member) + R4 (hinted handoff, not in the preference list)R3=v3; hint queued on R4The acking set {R3,R4} is no longer a subset of {R1,R2,R3}Write "succeeds" (W=2 met)
t5read, partition healed, hint not yet deliveredcontacts R1, R2 (R=2)sees v2 (R1), v1 (R2)No — write set was {R3,R4}, read set is {R1,R2}: disjointReturns v2 — v3 is silently missed

The pigeonhole argument only holds while every acking node is a genuine member of the key's preference list. The moment a sloppy quorum lets an outsider count toward W, the read set and write set can miss each other entirely — which is exactly why R + W > N is a normal-operation guarantee, not a partition-tolerant one. Which systems do this? Riak and the original Dynamo paper use sloppy quorums by design. Cassandra does not: its hinted handoff lets a hint satisfy a write only at consistency level ANY, so at ONE, QUORUM or higher the acks must come from real replicas and the overlap argument holds.

⚠ R + W > N does not give you linearizability It guarantees the newest committed value is in the read set. It does not order concurrent writes (two writers at W=2 can each succeed on a different pair and produce siblings), it does not make a failed write disappear (a write that reached one node and then failed can still be read later and repaired into existence), and sloppy quorums with hinted handoff — used by Riak and the original Dynamo, and by Cassandra only at CL=ANY — accept W acks from nodes that aren't even in the key's preference list, which breaks the overlap argument outright. Even with strict quorums, last-write-wins timestamps can pick the wrong winner under clock skew. Quorums buy you strong-ish reads under normal operation, not consensus. For real linearizability you need a consensus-backed log (Raft, Paxos) with reads confirmed by the leader (log, ReadIndex or a lease), or a read-repair-before-return protocol such as ABD. In Cassandra that means lightweight transactions and SERIAL reads, not QUORUM plus QUORUM.

Linearizability vs serializability — telling them apart for good

They sound like synonyms, they come from different fields, and mixing them up is the fastest way to lose credibility on this topic.

  • Linearizability is about single objects and real time. Every operation appears to take effect instantaneously at some point between its invocation and its response, and that point respects wall-clock order: if write W completes before read R begins, R must see W. It says nothing about multi-key transactions. It is a recency guarantee — the C in CAP.
  • Serializability is about multi-object transactions and says nothing about real time. Concurrent transactions produce a result equivalent to some serial order — and that order need not match the order they actually happened in. A transaction that committed an hour ago may legally be ordered after one committing now. It is an isolation guarantee — the I in ACID.
  • Strict serializability is both: a serial order that also respects real time. Spanner (via TrueTime) is the system that uses the term "external consistency" for it; FoundationDB documents strictly serializable transactions too. CockroachDB deliberately claims only serializability, because clock skew can produce a "causal reverse" anomaly between transactions that touch disjoint keys. Strict serializability is the strongest practical model.
A: write(x=1) B: read(x) C: read(x) overlaps the write → 0 or 1 both legal starts after → must return 1 write completes here real time → linearizability constrains only non-overlapping operations
Concurrency is where freedom lives: overlapping operations may be ordered either way, but once an operation has returned, everyone must see it.

The one-liner worth memorising: linearizability is about recency of a single key; serializability is about the illusion of one-at-a-time transactions. A database can be serializable and still let you read stale data: a transaction run against a lagging follower snapshot, or one serialised at an old timestamp, is a valid serial order that is not the newest one. (Postgres sidesteps this: a hot standby refuses SERIALIZABLE transactions outright.) And linearizable does not mean "no transactions": etcd offers atomic multi-key transactions and documents strict serializability for its default reads, while a single register with compare-and-set is linearizable with no multi-object transactions at all. ZooKeeper is the cautionary case: writes go through a leader in a total order, but reads are served locally by whichever server you are connected to and can be stale unless you call sync() first.

Between these sits snapshot isolation, which many databases ship as the strongest level they call "serializable" or as the default "repeatable read". It prevents dirty and non-repeatable reads, but still allows write skew (two doctors each see the other on call and both go off call), lost updates in some engines, and phantoms for range checks. The fix is a true serializable level, or an explicit constraint, SELECT ... FOR UPDATE, or a conditional write. Isolation levels get their own chapter; the point here is that "transactional" does not automatically mean "serializable".

Choosing per operation: model, cost, and what it looks like to a user

ModelGuaranteeCostReal product example
EventualReplicas converge if writes stopNone — local read, local write, survives partitionsLike count, view count, follower count. Off by 3 for 200 ms; nobody can tell, and nobody is harmed. Rendering it as "1.2k" makes the staleness literally invisible
Monotonic readsNever move backwards in timeSticky routing — mild load imbalance, awkward on replica failoverAn infinite feed. Items reappearing or vanishing while scrolling reads as a bug even when values are individually fine
Read-your-writesYou always see your own effectsPrimary reads for a short window, or LSN-waiting on readPosting a comment, editing a profile, uploading an avatar. The cheapest model that makes the product feel correct — and the default you should reach for on any write-then-read flow
CausalEffects never precede their causesDependency metadata on every write; real complexityThreaded comments and chat. A reply must not be visible before the message it replies to
Linearizable (single key)Reads see the latest committed write, in real-time orderLeader round trip; unavailable during leader loss (CP)Bank balance display, seat inventory, feature-flag kill switch. Being 200 ms stale is a wrong number on a screen someone will act on
Strict serializable / transactionalMulti-key transactions in a real-time-respecting serial orderConsensus per commit; cross-region commits cost an inter-region RTTUsername reservation, seat booking, money transfer. Two users claiming @ada concurrently must produce exactly one winner — this needs a uniqueness constraint or a compare-and-set, not a read-then-write
The three-question test for any field in your design
  • If a user sees a value 5 seconds old, what happens? Nothing → eventual. Confusing → session guarantees. Wrong decision or lost money → strong.
  • Can two concurrent writes both "win"? If the answer must be no (username, seat, inventory), no amount of replication tuning helps — you need a single serialisation point: a uniqueness constraint, a conditional write, or consensus.
  • Who notices the staleness — the writer or a third party? Only the writer → read-your-writes is enough and it's cheap. Everyone → you're in strong-consistency territory.
Say it like this → "I'd mix models rather than pick one. Counters and feeds go eventual off replicas. Anything the user just wrote gets read-your-writes via primary-pinning. Balance and inventory read from the leader, and the actual decrement is a conditional update inside a transaction, so two concurrent buyers can't both take the last seat. That's three models in one system and each one is justified by what breaks if I go weaker."

Recognizing it in an unseen problem

  • Signals: the moment your diagram grows a read replica, a cache, or a second region, you have chosen a consistency model — the only question is whether you chose it deliberately. "Multi-region", "read replicas", "global users" are all consistency prompts in disguise.
  • The naive design says "we'll use eventual consistency, it's more available" and then puts a write-then-read flow on top of it. Almost every user-visible consistency bug is a read-your-writes violation, not a deep causality problem.
  • Distinguishing it from CAP hand-waving: CAP is a statement about behaviour during a network partition, which is rare. Replication lag is present every single second of normal operation. Talk about lag; mention CAP only if partitions are actually in scope.
  • Uniqueness and inventory are not consistency-tunable. If two concurrent operations must not both succeed, no choice of R and W saves you — you need a single point of serialisation. Say that explicitly; it's a common trap.
  • Quantify. "Cross-region lag is ~100 ms, so a European read of a US write can be stale for a tenth of a second — fine for a like count, not for a payment confirmation" is worth more than three paragraphs of theory.
  • The pitfall to avoid: using "strongly consistent" and "serializable" interchangeably. If you only remember one thing: linearizability = recency of one key; serializability = transactions appear one-at-a-time.
Before you move on
  • Explain, using the pigeonhole argument, why R + W > N guarantees a reader sees the newest write during normal operation.
  • Trace what happens to that guarantee when a sloppy quorum with hinted handoff is in play during a partition, as in the dry run above.
  • State the difference between linearizability and serializability in one sentence each.
  • Pick a consistency model per operation for a concrete feature (a like count versus a username reservation, say) and justify each choice.
  • Explain why "we'll use eventual consistency" is an incomplete answer without also naming which specific operations need read-your-writes or strong consistency.
Sources and further reading
  • etcd API guarantees — atomic multi-key transactions, strict serializability, and the opt-in stale serializable reads
  • ZooKeeper programmer's guide — the stated guarantees, why reads can be stale, and when to call sync
  • Cassandra architecture: Dynamo — hinted handoff, and that a stored hint satisfies only consistency level ANY
  • PostgreSQL hot standby — SERIALIZABLE is not available on a standby
  • CockroachDB: living without atomic clocks — why it claims serializability, not external consistency, and the causal reverse anomaly
  • Spanner paper (OSDI 2012) — external consistency, TrueTime and commit wait
  • Spanner timestamp bounds — strong versus stale reads, and when stale reads can still block
  • Jepsen: strong serializability — the formal definition, and why it cannot stay available through a partition
  • FoundationDB transaction manifesto — strictly serializable transactions as a design goal

Claims in this chapter were checked against these sources in October 2026. Figures marked "planning figure" are rules of thumb, not guarantees.

✓Practise

5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.

I4

Finished reading?

0 of 36 chapters read in this series

→Next stop

  1. I3Previous · I3Message queues & async processing
  2. I4you are here
  3. Next · I5ReplicationKeeping copies of the same data on several machines is easy until the machines disagree, and then every design choice is a decision about which copy you are willing to lose.26 min read

01On this page

7 sections · 0%

  1. 01Consistency is a contract about what a read may return
  2. 02Replication lag is the concrete thing underneath all of it
  3. 03The two poles, and the useful middle
  4. 04Quorums: R + W N, with the numbers worked out
  5. 05Linearizability vs serializability — telling them apart for good
  6. 06Choosing per operation: model, cost, and what it looks like to a user
  7. 07Recognizing it in an unseen problem

[] chapters · t top