Cruxly › System design › Databases: Choosing One
Consistency models, for the person choosing
name the guarantee a user needs, pick the weakest model that gives it, and say what it costs
Open the interactive lessonConsistency is not one dial. It is a ladder of promises about which copy you read and in what order you see things, from strict serializable at the top to eventual at the bottom. Isolation is a different question, about overlapping transactions. A good answer names the guarantee each piece of data needs, the mechanism that provides it, what that costs in latency and availability, and how stale everything else may be.
Isolation and consistency are two different axes
The word "consistency" covers at least three things in interviews: the C in ACID (your application's invariants hold), the C in CAP (linearizability), and the freshness and ordering of replicated copies. This lesson is about the last two. First, separate them from isolation.
| Isolation | Consistency (replicas and order) | |
|---|---|---|
| Question | When transactions overlap, which interleavings are allowed? | After a write completes, who sees it, from which copy, in what order? |
| Lives in | The transaction manager of one database | Replicas, caches, regions, the client |
| Vocabulary | read committed, snapshot, serializable | linearizable, causal, read your writes, eventual |
| A failure looks like | Lost update, write skew | A stale read, a comment that vanishes |
| Fix | A stronger isolation level, locks, retries | Read the primary, wait for a replica, carry a token |
Two quick tests. If the bug needs two actors at the same moment, it is isolation. If one actor writes, then someone reads an old copy, it is consistency. A balance of 100 that two transactions both read and both subtract from is isolation. A balance of 70 on the primary that a replica still shows as 100 is consistency, and no isolation level changes that.
They get confused because both are fixed by "make it stronger", and because a single node hides the second axis completely. A replica read can also be perfectly isolated and still old: a transfer of 30 leaves the primary at 70 / 130 while a replica serves the earlier snapshot of 100 / 100. The total is right, nothing is torn, the data is simply behind.
Strict serializability is the model that covers both axes. Jepsen defines it as serializability's total order of multi-object transactions plus linearizability's real-time constraint: if A completes before B begins, A is ordered first. Spanner's documentation gives the banking example: a deposit commits, then a debit starts. A merely serializable system may reorder them, so the debit hits insufficient funds.
:::key Name the axis first Before you answer a "consistency" question, say "that is an isolation problem" or "that is a replica freshness problem". Many interview mistakes come from answering the wrong one. :::
The ladder, strongest to weakest
Jepsen's consistency map has many models, and it is a partial order, so a single ladder is a simplification. These are the ones that matter in products, with one example each and what the user sees when it breaks. The Alice and Bob example in the linearizable row is Kleppmann's (his 2015 post, set at the 2014 World Cup final); the lunch chat used later for causal consistency is Jepsen's.
| Model | Promise | Violation looks like | Under a partition (Jepsen) |
|---|---|---|---|
| Strict serializable | Transactions in one order that respects real time, across many objects | Reordered transactions: the debit ran "before" the deposit it followed | Cannot be totally or sticky available |
| Linearizable | One object. If A finishes before B starts, B sees A or newer | Alice sees the final score and tells Bob; Bob reloads a lagging replica and the game is still on | Cannot be totally or sticky available |
| Sequential | One total order everyone agrees on, consistent with each client's own order, but not tied to the clock | Two users see events in different orders. (A stale read alone is allowed.) | Cannot be totally or sticky available |
| Causal | Cause before effect everywhere. Unrelated events may differ | A reply visible before the question it answers | Sticky available |
| Session guarantees | Per client: read your writes, monotonic reads, monotonic writes, writes follow reads | Your own edit disappears; a comment appears then vanishes | See next section |
| Eventual | If writes stop, copies converge. Nothing is promised in between | Any old state, in any order, for an unspecified time | Available (no coordination needed) |
Three notes that interviewers probe.
- Linearizable is a single-object model; strict serializable is the transactional, multi-object version. Sequential drops the real-time clause, so a reader may be far behind but never goes backwards once it has seen something.
- Jepsen: causal consistency "captures" that related operations appear in the same order on all processes while independent ones may differ. Its chat example: nobody ever sees "yes" before "shall we have lunch?".
- Eventual does not mean wrong. Cassandra's own guarantees page defines it as updates eventually reaching all replicas, with divergent versions existing temporarily and then reconciled. The data is old, not corrupt. Whether concurrent writes are reconciled safely is a separate question, about conflict resolution.
:::key Read the table as a menu with prices Each step up buys a promise a user can notice, and costs coordination: latency on the read or write path, and availability when the network splits. :::
Session guarantees: the cheap ones users actually notice
Session guarantees constrain what one client sees, not what everyone sees. That is why they are cheap and why they are the guarantees real users complain about.
| Guarantee | Meaning | Violation the user sees | Typical fix |
|---|---|---|---|
| Read your writes | After my write, my reads reflect it | I save my name, reload, the old name is back | Read the primary briefly, or carry a version token |
| Monotonic reads | My reads never go back in time | A comment appears, then vanishes on refresh | Pin the session to one replica |
| Monotonic writes | My writes are applied in the order I issued them | I set "draft" then "published", a replica applies them in reverse and stays on draft | Order writes per client at the replica |
| Writes follow reads | A write I make after reading lands after what I read | My reply shows up where the post it answers does not exist yet | Ship the dependency with the write |
Availability is what makes this group attractive. Jepsen classes monotonic reads, monotonic writes and writes follow reads as totally available: every node can keep serving during a partition. Read your writes is sticky available: it holds as long as the client keeps talking to the same server. Jepsen's causal page says it plainly: if you need total availability you give up causal consistency and read your writes, but you can still have the other three.
One trap: monotonic reads does not give read your writes. A session pinned to a replica that is consistently ten seconds behind never goes backwards, yet it never sees the write it just made either.
MongoDB's docs list exactly these four as the causal consistency guarantees of a client session, and state that they hold together only with majority read concern and majority write concern. That is a good example of a vendor selling "causal" as a bundle of session guarantees.
Quorum stores: DynamoDB and Cassandra
DynamoDB (AWS docs, read October 2026). Reads of tables and local secondary indexes are eventually consistent by default; setting ConsistentRead to true returns data reflecting all earlier successful writes. Reads from global secondary indexes and streams are always eventually consistent; strong reads there are not supported. Eventual reads cost half: a read unit covers one strong read or two eventual reads per second of up to 4 KB. Example (an assumption): 400 reads per second of 3 KB items need 200 read units eventually consistent and 400 strongly consistent.
Global tables add a second dimension. Multi-Region eventual consistency (MREC, the default) replicates typically within a second, and a strongly consistent read returns the latest only if the item was last written in that Region. Multi-Region strong consistency (MRSC) replicates synchronously to at least one other Region before the write returns, so a strong read on any replica is current, at higher write and strong-read latency. The docs list limits for MRSC tables: exactly three Regions (three replicas, or two plus a witness), no transactions, no TTL, no local secondary indexes.
Cassandra (docs read October 2026). You pick a consistency level per request. With replication factor RF = 3:
| Write at | Read at | W + R > 3? | Reads see the last acked write? | Copies you can lose |
|---|---|---|---|---|
| ONE | ONE | no (1+1) | No, can miss it | 2 for either |
| QUORUM (2) | QUORUM (2) | yes (2+2) | Yes, the sets overlap | 1 for either |
| QUORUM (2) | ONE | no (2+1) | No | 1 write, 2 read |
| ONE | ALL | yes (1+3) | Yes | 2 write, 0 read |
| ALL | ONE | yes (3+1) | Yes | 0 write, 2 read |
Quorum is a majority, floor(RF/2) + 1. Writes are always sent to all replicas; the level only decides how many replies the coordinator waits for. LOCAL_QUORUM limits the wait to the local datacenter, which the docs describe as enough for clients homed in one datacenter to read their own writes. The Dynamo paper's common setting was (N, R, W) = (3, 2, 2).
Overlap is not linearizability. Kleppmann warns against relying on it: sloppy quorums and read repair create edge cases where deleted data returns or the replica count drifts from what the quorum assumed. Cassandra itself separates the two: writes to a table are eventually consistent, and lightweight transactions (Paxos) give linearizable compare-and-set on a single partition. Use them for the few operations that need a single winner, such as claiming a username.
Primary and replica systems: PostgreSQL, MySQL, MongoDB, Redis
Most production systems are one writer plus replicas. The default is almost always: the primary is current, replicas are stale by some amount. What each product lets you change:
| System | Default | Turn it up | Price |
|---|---|---|---|
| PostgreSQL | Streaming replication is asynchronous. Docs: a small delay, typically under a second if the standby keeps up. | Set synchronous_standby_names and synchronous_commit = remote_apply: commit waits until the standby has replayed it and made it visible | At least one round trip added to each commit; the setting can be chosen per transaction |
| MySQL 8.4 | Asynchronous. | Semi-sync: the source waits for a replica to receive and log the events | Acknowledges receipt, not apply, so replica reads can still be stale. If no replica answers in time the source falls back to async |
| MongoDB | Write concern majority; read concern local. Secondary reads can lag but are never out of order | Causal session with majority read and write concern for all four session guarantees; linearizable read concern for single-document recency | Linearizable reads can be much slower; set maxTimeMS so the read errors instead of blocking when a majority is unavailable |
| Redis | Asynchronous replication | WAIT n timeout blocks until n replicas acknowledge | Lowers loss risk, but Redis states WAIT does not make it a strongly consistent store: acknowledged writes can still be lost at failover |
Details worth saying aloud:
- PostgreSQL's docs note that
remote_apply"allows for load balancing with causal consistency" in simple cases. That is the clean answer to "how do I read my writes from a replica": make the commit wait. - MySQL's semi-sync protects against losing a commit on failover. It was never a freshness guarantee. Group Replication is the MySQL feature for servers that apply the same transactions in the same order.
- MongoDB's default write concern is majority except in some arbiter layouts, where it is
w: 1. Causal sessions guarantee all four session properties only for majority read and majority write concern;localreads withw: 1cannot. A session must be used by one thread at a time. A former primary cut off by a partition can serve stale data, which is whylinearizableexists: it confirms with a majority before answering. - Redis Cluster uses asynchronous replication and its docs say there are always windows where acknowledged writes can be lost, larger for clients on the minority side of a partition.
What strength costs, and buying it back with bounded staleness
Strength is paid in two currencies. Latency: a write or a fresh read must wait for coordination, and across regions that is a speed-of-light round trip. Availability: Jepsen classes linearizable, sequential and strict serializable as unable to be totally or sticky available, so some or all nodes cannot make progress during a partition.
Abadi's PACELC states it without the CAP slogans: if there is a partition (P), trade availability against consistency (A and C); else (E), trade latency against consistency (L and C). The "else" half is the one that matters daily: any replicated system faces it when nothing is broken, and over a wide-area network there is no way around it. The paper puts the default Dynamo, Cassandra and Riak in the PA/EL group. Kleppmann argues that CAP labels are too coarse, since many systems are neither linearizable nor CAP-available, and says to state the concrete guarantee and failure behaviour. In an interview, say "reads go through the leader; during a partition the minority side rejects them; each fresh read costs a cross-region round trip".
One data point on cost: in the Spanner paper's 2012 microbenchmark (OSDI 2012, Table 3; one replica), commit wait was about 5 ms, Paxos about 9 ms, and a 4 KB write took about 14 ms in total, while read-only transactions and snapshot reads took about 1.3 to 1.4 ms. Clients and servers sat less than 1 ms apart and reads came from memory, so these show software overhead, not cross-region cost; treat them as a 2012 data point, not a current benchmark.
Bounded staleness buys the latency back.
| System | Stale option | Detail from the docs |
|---|---|---|
| Spanner | Strong (default), bounded staleness, exact staleness | Stale reads help most at long staleness; the timestamp-bounds page suggests a minimum of 10 seconds (the reads page calls 15 seconds a reasonable value) |
| CockroachDB | Exact staleness via follower_read_timestamp(); bounded via with_max_staleness or with_min_timestamp | Exact reads should be at least about 4.2 s in the past; bounded reads must be a single statement on one row and keep serving even when the leaseholder is unreachable, with answers growing older |
| CockroachDB | Strong follower reads on GLOBAL tables | Fast reads from every region, slower writes |
| Postgres, MySQL | No built-in bounded-staleness read | You measure replica lag and enforce the bound in your router |
How to state a bound. A usable sentence has a number, a percentile and a fallback: "Replica reads may be at most 5 seconds behind (an assumed figure) at p99; the router skips any replica past that lag, and reads then go to the primary." Anything less, such as "mostly fresh", cannot be tested or alerted on.
Client-side techniques and what they cost
When the database default is "replica reads may be stale", the application restores a guarantee for the users who notice. Three techniques, from blunt to precise.
| Technique | Gives | How | Cost |
|---|---|---|---|
| Primary after write | Read your writes | For N seconds after a user writes, send that user's reads to the primary | Extra primary load; N is a tuned number (choose it longer than normal replica lag); a lag spike beyond N leaves a gap |
| Version token | Read your writes, causal order | The write returns a log position or timestamp; the next read carries it; a replica answers only if it has reached it, otherwise it waits briefly or the read goes to the primary | The token travels with every request; replicas must expose their position |
| Sticky routing | Monotonic reads | Pin a session to one replica | Uneven load. It does not guarantee the replica has your write. If the replica dies, the session lands on one that may be further behind |
Real systems implement the token idea. MongoDB causal sessions carry operation and cluster time between operations. CockroachDB's with_min_timestamp exists, in its docs' words, to enforce causality between an operation and the reads that depend on it. Jepsen notes read your writes is sticky available only if the client never changes servers, which is the sticky routing trade in one line.
Two details separate good answers from adequate ones.
- Cross-device. A phone and a laptop are different sessions. If the token or the "recently wrote" flag lives in a session cookie, the laptop does not see the phone's edit. Key it to the user.
- Only the writer notices. Other readers seeing the old value for a second is usually fine. That is why per-user techniques beat making every read strong.
Mapping product requirements to the weakest safe model
The rule: pick the weakest model whose violation your users cannot see, or do not mind. Ask three questions out loud for each piece of data: who sees it, what do they do with it, and what does a stale value cost?
| Feature | Weakest safe model | Reasoning to say | Mechanism |
|---|---|---|---|
| Profile edit | Read your writes for the editor; eventual for everyone else | Only the editor can tell; others can lag seconds | Primary after write or a token |
| Feed / timeline | Eventual with a bound, plus monotonic reads | Late posts are invisible; posts that appear then vanish are not | Replicas with a pinned session |
| Inventory | Linearizable on the decrement; eventual on the "3 left" badge | A wrong badge is harmless; selling the last item twice costs money | Conditional update on the primary |
| Payments / ledger | Serializable transactions on the primary; strict serializable if real-time order must hold across regions | A stale balance can approve a second spend | Primary reads, idempotency keys |
| Chat read receipts | Causal order within a conversation, receipts that only move forward | A reply before its question confuses people; a receipt must never un-read | Per-conversation ordering, max-merge receipts |
| Leaderboard | Eventual, with a stated bound | Nobody can tell it is five seconds old | Any replica or cache; final results from the record |
The inventory row shows the habit worth practising: split one feature into its stale-safe part and its strict part. Spending strength only on the buy step keeps the strong path small, cheap and fast, and leaves the rest of the system free to be replicated, cached and available.
If an interviewer pushes ("why not make everything strong?"), the answer has three parts: latency on every read, lost availability during partitions, and wasted money on data nobody can see is stale.
Interview: common wrong statements and the right answers
| Wrong statement | Why it is wrong | Say instead |
|---|---|---|
| "Eventual consistency means the data may be wrong." | It means copies converge once writes stop. In between, reads can be old. Conflicting writes need a resolution rule, which is a separate issue | "Reads may be up to N seconds old, and replicas converge." |
| "Strong consistency means ACID." | ACID describes transactions. Linearizability is a recency and ordering guarantee on replicated copies | "ACID for the transaction, linearizable reads for freshness." |
| "SQL is strongly consistent, NoSQL is eventual." | DynamoDB offers ConsistentRead, MongoDB has read concerns, and a PostgreSQL async replica is stale | "It depends on the read path and settings." |
| "Quorum reads and writes are linearizable." | Overlap shows a recent write, not a real-time order; sloppy quorums and read repair break the assumption | "Quorum gives fresh reads in normal operation; use lightweight transactions for compare and set." |
| "Reading the leader is always fresh." | A former primary cut off by a partition can still answer, which is why MongoDB has a linearizable read concern | "Fresh reads need confirmation, not just a role." |
| "Synchronous replication means replicas are always fresh." | MySQL semi-sync acknowledges receipt, and PostgreSQL's default on waits for the standby's flush, not apply | "Use remote_apply, or a token." |
| "This database is CP / AP." | Kleppmann: CAP's terms are narrow, and many systems are neither. Settings change the answer | "Name the guarantee and what happens in a partition." |
| "Redis WAIT makes it consistent." | Redis says WAIT does not make Redis a strongly consistent store; a write replicated to several replicas can still be lost | "WAIT reduces the loss window." |
The four-part answer for any piece of data:
- The guarantee it needs (linearizable, read your writes, causal, eventual with a bound).
- The mechanism that provides it (primary read, quorum, token, remote_apply, lightweight transaction).
- The price, in latency and in availability during a partition.
- The staleness you accept elsewhere, as a number, so the strong path stays small.
Likely follow-ups: "What happens during a partition?" (the minority side of a strong system rejects; an eventual one answers with older data), "How do you detect staleness?" (measure replica lag and enforce a bound), "What would you change at ten times the traffic?" (more replicas make staleness a bigger share of reads, so the token and fallback matter more).
How to say it in the interview
"Which consistency, for which data? The balance and the last item in stock need a read that cannot be stale, so they go to the primary or a linearizable path. The profile page needs read your writes for the editor and seconds of staleness for everyone else. The feed and the leaderboard can be eventual with a stated bound. I'd pay for strength only where an old read costs money."
Checklist
- Separated isolation (overlapping transactions) from consistency (replica freshness and order) before answering
- Named the model by its promise and its violation, not by a label
- Picked session guarantees (read your writes, monotonic reads) for per-user problems
- Did the quorum arithmetic: R + W > RF, and what each level costs in failures tolerated
- Said how to turn consistency up in the specific system and what it costs
- Stated staleness as a number, a percentile and a fallback
- Mapped each feature to the weakest safe model and spoke the reasoning aloud
- Avoided the wrong statements: eventual means wrong, quorum is linearizable, CP/AP labels, strong means ACID