The CAP theorem in depth
You never "pick two" — you pick one letter during a partition, and PACELC tells you what you picked for the other 99.9% of the time.
The version everyone repeats is wrong, and the wrongness matters
The folklore statement is "consistency, availability, partition tolerance — pick two." That framing fails the moment you take it seriously, because P is not a choice you get to make. A partition is a property of the network: a cut fibre, a failed top-of-rack switch, a bad BGP announcement, an AZ losing connectivity, or — most commonly — a node that is merely slow (a 40-second stop-the-world GC pause, a VM live-migration stall) and is therefore indistinguishable from a dead one. You cannot buy a network that never partitions, so "CA" is not a system design. It is a system with no story for partitions, which under partition quietly gives up both letters.
The precise statement is narrower and far more useful: when a partition occurs, and only then, you must choose between consistency and availability. When there is no partition — which is essentially all of the time — CAP says nothing whatsoever about your system. That last clause is the one that separates candidates: CAP is a theorem about the rare case, and the rare case is not where your latency budget lives.
The three letters, defined precisely (this is where marks are lost)
| Letter | What it formally means | What candidates think it means |
|---|---|---|
| C | Linearizability. The system behaves as if there is exactly one copy of the data: every read returns the value of the most recently completed write, and once a read sees a value, no later read sees an older one. | The C in ACID (integrity constraints). Unrelated. |
| A | Every request to a non-failing node returns a non-error response. No bound on how long it takes, and no promise the answer is fresh. | "Four nines of uptime." Also unrelated — a CP system can have a better SLA than an AP one. |
| P | The system keeps operating when arbitrary messages between nodes are dropped. | An optional feature. It is not optional. |
Two consequences fall straight out of those definitions. First, CAP-A has no latency bound, so a system that answers in 30 seconds is "available" — which is why CAP alone is useless for reasoning about production SLOs. Second, CAP-C is linearizability, the strongest single-object guarantee; weaker useful models (read-your-writes, monotonic reads, causal consistency) sit below it and are perfectly compatible with staying available during a partition. "AP" does not mean "no guarantees."
What CP actually looks like from the client's seat
Take a five-node leader-based cluster — etcd, ZooKeeper, a Spanner Paxos group, a MongoDB replica set with majority write concern. The network splits it 3 | 2.
- Majority side (3 nodes): keeps or elects a leader, keeps committing writes, fully functional. If the old leader was on this side, there is no interruption at all.
- Minority side (2 nodes): cannot reach a quorum, so it cannot commit anything. Writes fail immediately with something like "no leader" or a context deadline. Reads either fail too, or are served locally and are knowingly stale — that is a per-system decision, and a good one to ask about.
- A client pinned to the minority sees hard errors for the entire duration of the partition, plus an election timeout on top (Raft's classic 150–300 ms, etcd's default 1000 ms, ZooKeeper tick-based failover typically a few seconds).
The client-visible contract is: never a wrong answer, sometimes no answer. That is the right trade whenever a wrong answer is more expensive than an error page — a ledger balance, decrementing the last unit of inventory, a cluster-membership record, a lock, a feature-flag kill switch.
What AP actually looks like from the client's seat
Now the same partition in Cassandra at consistency level ONE, or DynamoDB with eventually-consistent reads, or Riak. Both sides accept writes. Every client gets a 200. Nothing appears wrong — and that is precisely the danger, because the damage is deferred to reconciliation time. When the partition heals you have two divergent histories, and something has to merge them:
| Reconciliation strategy | What it costs | Reach for it when… |
|---|---|---|
| Last-write-wins by timestamp | Silently discards one side's writes. With wall-clock timestamps, clock skew of a few hundred ms decides which user's data survives. | The value is a cache-like overwrite where losing an update is genuinely acceptable — a "last seen at" field, a presence flag. |
| Siblings / vector clocks returned to the app | Every read path must now handle "here are 3 conflicting values." Real complexity, pushed into product code. | The app has domain knowledge that makes merging obvious (union the shopping cart). |
| CRDTs (G-Counter, OR-Set, LWW-Register) | Restricted data types, metadata growth, and you must model the domain as a mergeable lattice. | Counters, sets, collaborative text, presence — anywhere concurrent edits are normal (Redis CRDT, Automerge, Yjs). |
| Read repair + anti-entropy (Merkle trees) | Background convergence only; does not decide semantics, just propagates whatever the winner already is. | Always — it is a complement to one of the above, never a substitute. |
PACELC — the framing that actually predicts database behaviour
Abadi's 2010 extension is the one to bring up unprompted, because it covers the case CAP ignores: if there is a Partition, choose A or C; Else, choose Latency or Consistency. The "else" half applies over 99.9% of your system's life, and it is not a legal technicality — it is forced by physics. Keeping replicas linearizable requires a round trip to a quorum before you can answer, and round trips cost:
| Hop | Typical RTT | What that means for a quorum write |
|---|---|---|
| Same rack / same AZ | 0.2–0.5 ms | Consistency is nearly free; take it. |
| Cross-AZ, same region | 0.5–2 ms | Still cheap. This is why a 3-AZ quorum inside one region is the default shape for most CP systems. |
| us-east ↔ us-west | 60–70 ms | A cross-region quorum write costs at minimum the second-fastest RTT. Your write p50 now starts at ~35 ms. |
| us-east ↔ eu-west | 75–90 ms | Global linearizable writes are tens of milliseconds, permanently. No amount of engineering removes this. |
Spanner is the clearest illustration of choosing consistency in the "else" branch and simply paying: it is PC/EC, it commits through Paxos across regions, and it adds a deliberate commit wait of roughly twice the TrueTime clock uncertainty (single-digit milliseconds) so that commit timestamps are globally meaningful. Writes land in the tens of milliseconds. In exchange you get external consistency across a planet, and read-only transactions at a past timestamp that any replica can serve locally with no coordination at all.
Where real systems sit — the honest, tunable version
| System | PACELC | Why, and where the knob is |
|---|---|---|
| Postgres, single primary + sync standby | PC/EC | With synchronous_commit = on and a named standby, a commit waits for the standby. Lose the standby and writes block — that is CP behaving as advertised. Switch to local/async and you become EL with replica reads stale by the replication lag (sub-ms idle, seconds-to-minutes under a heavy write burst or a long-running query on the replica). |
| DynamoDB | PA/EL (default) PC/EC (per read) | Reads are eventually consistent by default and served by any of the three AZ replicas. Pass ConsistentRead=true and you route to the leader replica: 2× the read cost, a few extra ms, and unavailable if that partition has no leader. The letter is chosen per API call. |
| Cassandra / ScyllaDB | PA/EL (default) PC/EC (QUORUM) | Consistency level is per query. ONE is fast and stale-tolerant; QUORUM read + QUORUM write gives R + W > N and single-key linearizability-ish behaviour at the cost of latency; LOCAL_QUORUM keeps you inside one DC. Note that sloppy quorums with hinted handoff can violate the overlap guarantee during a partition — worth knowing. |
| Spanner / CockroachDB | PC/EC | Paxos/Raft per range, majority commit. Consistency is not negotiable; latency is the bill. Follower reads and bounded-staleness reads are the escape hatch when you want EL for a specific query. |
| ZooKeeper | PC/EC (writes) | Zab totally orders writes through a leader with a majority. But reads are served locally by any follower and may be stale — you must call sync() first for a linearizable read. Saying this unprompted is a strong signal. |
| etcd | PC/EC | Raft; linearizable reads by default via read-index. Minority members return errors. This is the coordination substrate for Kubernetes precisely because it refuses to guess. |
| MongoDB | PC/EC or PA/EL | w:majority + readConcern:majority is CP-ish; w:1 plus reads from secondaries is AP-ish and can lose acknowledged writes on failover (rollback files). |
| Redis (Sentinel or Cluster) | Neither, honestly | Replication is always asynchronous, so a failover can lose writes the primary already acknowledged. It is AP-shaped without AP's conflict resolution — the losing writes just vanish. WAIT reduces the window but is not a quorum commit. Treat Redis as a cache or a lossy store, and never as your source of truth for money. |
The quorum dial: R + W > N
In a Dynamo-style system with N replicas, you require W acknowledgements to accept a write and R responses to serve a read. If R + W > N, the read set and the write set must overlap in at least one replica, so every read touches at least one node that saw the latest write.
| N=3 config | Behaviour | Reach for this when… |
|---|---|---|
| W=1, R=1 | Fastest possible both ways, no overlap, reads can be arbitrarily stale. One replica loss loses data. | Metrics, logs, view counters, anything where losing a write is a rounding error. |
| W=2, R=2 | Overlap guaranteed. Tolerates one node down for both reads and writes. | The default. Most OLTP-ish workloads on a Dynamo-style store. |
| W=3, R=1 | Reads are single-hop fast; any node down makes writes unavailable. | Read-dominated config data written rarely. |
| W=1, R=3 | Writes are single-hop fast; any node down makes reads unavailable. | Rarely correct — usually a sign the workload wants W=2, R=2. |
Using CAP in the room without sounding like a flashcard
The senior move is to refuse the question as posed. Nobody designs "a CP system"; you design a system in which different data has different requirements, and you say so explicitly:
- Payments ledger, account balances, idempotency keys: CP. Refuse the write rather than double-charge.
- Session store, presence, notification counts, feed ranking: AP. A stale unread count costs nothing.
- Inventory: split it. The last 50 units of a SKU are sold through a CP path with a real reservation; the "1,200 in stock" badge on the listing page is AP and can be minutes old. Ticketmaster-style seat selection is the same trick — browse is AP, hold is CP.
- Cluster metadata, leader election, feature-flag kill switches: CP, and delegated to etcd or ZooKeeper rather than built.
Recognizing it in an unseen problem
- The prompt spans more than one datacentre, region, or AZ — the moment replicas can be separated by a network, CAP is live and the interviewer will probe it.
- Words like "globally distributed", "multi-region active-active", "must never lose a write", "must always accept a write" are direct invitations to name your partition-time choice.
- A naive design says "we'll replicate to the other region" and stops. The follow-up that exposes it is always: what does the client see while the link between them is down? Have that answer ready before it's asked.
- Distinguish from the plain replication chapter: replication is about how copies get updated; CAP is about what you do when they can't. Distinguish from PACELC's else-branch: if the network is healthy and you are still arguing about staleness, that is a latency-vs-consistency question, not a CAP question.
- If you choose AP, immediately state the merge function. If you choose CP, immediately state the blast radius: which clients get errors, for how long, and what the retry/queue story is so the user doesn't just see a 500.
- The strongest closing move is to make the choice per-operation and justify each one with the business cost of being wrong versus the business cost of being down.