Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

24 chapters
⌕
Beginner7›
B1The mental modelB2Client-server basicsB3Vertical vs horizontalB4Databases in designB5Caching fundamentalsB6APIs & communicationB7Walkthrough: URL shortener
Intermediate9›
I1Load balancing in depthI2CDNsI3Message queues & asyncI4Consistency modelsI5Rate limiting & throttlingI6Designing for availabilityI7Storage systemsI8Search systemsI9Walkthrough: feed/chat
Advanced8›
A1CAP theorem in depthA2Sharding at scaleA3Distributed consensusA4Fault toleranceA5Observability at scaleA6Capacity estimationA7Case studiesA8Tradeoff thinking
/ search[ ] chaptert top

System Design levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I4

Consistency models

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

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 balance went down. 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)70-120 ms, floor set by RTTSeconds 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 — route to the leader, or read a quorum, or hold a lease. Cost: at least one extra round trip, and unavailability whenever the leader 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
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.

⚠ 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 — the default in Dynamo-style systems during a partition — accept W acks from nodes that aren't even in the key's preference list, which breaks the overlap argument outright. Quorums buy you strong-ish reads under normal operation, not consensus. For real linearizability you need Raft or Paxos with a leader lease.

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. This is what Spanner (via TrueTime), FaunaDB and CockroachDB market as "external consistency", and it 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 (Postgres SERIALIZABLE on a replica); a store can be linearizable per key and have no transactions at all (etcd, ZooKeeper).

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.
←previousMessage queues & async↑ CovernextRate limiting & throttling→