Ch. 19 · System Design

System Design Consistency and User Expectations

Read-your-writes, monotonic reads and linearizability tied to real product decisions, with a replica-lag simulation of what users see.

~6 min readintermediateupdated Oct 11, 2026

“Is the system strongly or eventually consistent?” sounds like a reasonable interview question, and it has a bad answer: picking one. Real systems mix guarantees. A social feed can show a post two seconds late without anyone noticing. A card authorisation can’t approve a purchase against a credit limit that was used up a second ago. Both features might sit on the same database cluster.

The useful skill is matching each operation to the weakest guarantee that keeps users from seeing something wrong, then paying for stronger guarantees only where they matter.

A ladder of guarantees, in user terms

Consistency models describe which writes a reader is allowed to see. Here they are from strongest to weakest, each with the anomaly it rules out:

Linearizable. Every read sees the latest completed write, as if there were one copy of the data. If a gift card’s balance dropped to zero at 10:00:00.100, no read after that instant anywhere sees the old balance. This is what people usually mean by “strong consistency”, and it costs coordination: reads go to a leader or a quorum, and a network partition can force the system to refuse requests rather than answer stale.

Causal. If one write could have influenced another, everyone sees them in that order. A reply never appears before the comment it answers. Unrelated writes may appear in different orders to different readers.

Read-your-writes. A user always sees their own changes. After you edit your bio and refresh, you see the new bio, even if other people see the old one for a moment.

Monotonic reads. Once you’ve seen a value, you never see an older one. Without it, refreshing a page can make a new comment appear, then disappear, then reappear.

Eventual. If writes stop, all replicas converge. It says nothing about how long that takes or what you see meanwhile.

A common misconception is that “eventual” means “a few seconds later”. It’s a guarantee about convergence, not about time. Usually lag is milliseconds; during a network problem or a long-running query on a replica it can be minutes, and an eventually consistent design has to be acceptable in that case too.

What users see when replicas lag

Most user-visible consistency bugs come from reading a follower that hasn’t applied a write yet. To put numbers on it, here’s a simulation in Node: one leader and two followers, where a follower usually applies a write about 100 ms after the leader but is around two seconds behind 5% of the time. A user writes, waits, then reads. Three routing policies are compared:

if (policy === 'any replica') pick = Math.floor(rand() * 3);
else if (policy === 'leader for 1 s after a write') pick = waitMs < 1000 ? 0 : Math.floor(rand() * 3);
else if (policy === 'session token') {       // a follower answers only if it has applied the client's last write
  const f = 1 + Math.floor(rand() * 2);
  pick = replicas[f] <= waitMs ? f : 0;
}
JavaScript

Over 100,000 trials per cell:

read-your-writes: % of reads that miss the user's own write
policy                          wait 50ms   wait 300ms   wait 3s
any replica                        41.88%        6.52%        0.42%
leader for 1 s after a write        0.00%        0.00%        0.45%
session token                       0.00%        0.00%        0.00%
monotonic reads: new value then old value on refresh: random replica 3.45%, same replica 0.00%
Text

Reading from any replica, 42% of reads 50 ms after a write miss it. That’s the classic “I saved my profile and it reverted” bug report. Even three seconds later, 0.42% still miss, because of the occasional badly lagging follower.

The common fix, “read from the leader for a second after a user writes”, works inside its window. But look at the three-second column: 0.45% of reads miss, because once the window ends the user can land on a follower that’s more than three seconds behind. A fixed window is a guess about lag, and lag doesn’t respect guesses.

The session token approach has no window. After a write, the client keeps the position of that write in the replication log (PostgreSQL calls it an LSN; MySQL has GTIDs; MongoDB has cluster time for causal sessions). A follower may answer only if it has applied at least that position; otherwise the read goes to the leader. Zero misses in every column, and most reads still go to followers.

The last line shows monotonic reads. When each refresh picks a random replica, 3.45% of users who saw the new value saw the old one on the next refresh. Pinning a user’s session to one replica (by hashing the user id) removes that completely, because a replica never un-applies a write.

Matching guarantees to product decisions

Take a credit card app and go feature by feature:

Operation What goes wrong if stale Guarantee How
Transaction history list A purchase appears a second late Eventual, plus read-your-writes for the user’s own actions Read followers; session token after the user acts
Push notification count Badge shows 3 instead of 4 briefly Eventual Any replica or a cache
Changing the card PIN User thinks the change failed and retries Read-your-writes Session token or leader read after the change
Authorising a purchase against the credit limit Two purchases both approved past the limit Linearizable for that check Conditional write on the leader
Monthly statement Must match the ledger exactly Snapshot of committed data Read from a consistent snapshot after the period closes

The authorisation row deserves a closer look, because “read the latest balance, then decide” is still wrong even on the leader. Two authorisations can both read 500 available, both approve 400, and leave the account at minus 300. The fix is to make the check and the change one atomic operation: UPDATE accounts SET available = available - 400 WHERE id = 7 AND available >= 400, and treat zero rows updated as a decline. That’s the database enforcing the invariant, which is stronger than any read guarantee.

Quorums and their fine print

Leaderless stores such as Cassandra and DynamoDB-style systems use quorums. With N copies, a write waits for W acknowledgements and a read asks R replicas. If R + W > N, every read set overlaps every write set, so at least one replica in the read has the latest acknowledged write. With N = 3, W = 2, R = 2 that holds; with W = 1, R = 1 it doesn’t.

The fine print interviewers like: overlap doesn’t make the system linearizable on its own. Concurrent writes still need a rule for which wins, and “last write wins” by wall-clock timestamp silently drops one of them when clocks disagree. Sloppy quorums (accepting writes on stand-in nodes during failures) also break the overlap until repair runs. Say what happens to conflicting writes, not just the R and W numbers.

Interview practice: the vanishing comment

“Users report that after posting a comment, it sometimes isn’t there when the page reloads, and occasionally it appears and then disappears on a second refresh. Comments are written to a PostgreSQL primary and read from two streaming replicas behind a load balancer. What’s happening, and what would you change?”

Answer

Both symptoms come from reading lagging replicas. The missing comment is a read-your-writes violation: the reload hit a replica that hadn’t applied the insert. The appear-then-disappear pattern is a monotonic reads violation: the first refresh hit an up-to-date replica, the second a lagging one.

I wouldn’t send all reads to the primary, since that throws away the replicas’ capacity for a problem that affects one user per write. Instead, after a user posts, the API returns the commit’s LSN (from pg_current_wal_lsn() after the insert) and the client sends it on the next few reads. The read path checks a replica’s pg_last_wal_replay_lsn() and uses it only if it has replayed past that position, falling back to the primary otherwise. For monotonic reads, I’d route each session to a consistent replica by hashing the user id, failing over only if that replica is unhealthy. Other users can keep seeing the comment a few hundred milliseconds late, which is fine for comments. I’d also add a replica lag metric and alert, so a replica that falls minutes behind gets taken out of rotation instead of serving very old data.

Further reading

More in System Design

read ✓System Design · easy

System Design Cache-Aside and Stale Data

Cache-aside read and write paths, why you update the database before deleting the key, and the race that still leaves stale data until TTL.

~7 min readread →
read ✓System Design · mid

System Design Replication and Read Freshness

Leader-follower replication in numbers: commit latency for sync and quorum acks, how a batch job builds 150 s of lag, and failover loss.

~6 min readread →
esc