Consistency, Availability, and the CAP/PACELC Trade-off
Consistency and availability trade-offs show up in nearly every system design discussion. The theory (CAP, PACELC) is well-known; the practical application — knowing which choice to make for a specific use case — is what separates a design-literate engineer from one who just quotes theorems.
CAP states that in the presence of a network partition, a distributed system must choose between Consistency (all nodes see the same data at the same time) and Availability (every request receives a response, though it may be stale).
What CAP doesn’t mean:
- It’s not a binary permanent choice — modern systems tune per-operation consistency
- “Consistent” in CAP means linearizable consistency (strongest form) — not just “data is sometimes accurate”
- Network partitions are rare but inevitable. The real question is “what do you do when they happen?”
The practical framing: Most distributed systems are not in a constant state of partition. The everyday trade-off isn’t about partitions — it’s about consistency vs latency, which is what PACELC addresses.
PACELC: During a Partition, choose between Availability and Consistency. Else (normal operation), choose between Latency and Consistency.
The “Else” clause is what matters day-to-day. In normal operation:
- Consistent reads require coordinating with enough replicas to guarantee the latest write is seen. This takes time.
- Low-latency reads can return from the nearest replica, which may be slightly behind.
This is the everyday trade-off: do you pay latency for consistency, or accept some staleness for speed?
Database examples:
- Postgres (single node): PC/EC — consistent, not distributed
- Cassandra: PA/EL — prefers availability during partition, low latency over consistency in normal operation. Tunable.
- DynamoDB: PA/EL by default, PA/EC with strong consistent reads option
- Spanner/CockroachDB: PC/EC — global strong consistency via TrueTime / HLC. You pay the latency.
- ZooKeeper: PC/EC — consistency over availability
Eventual consistency means: if no new updates are made, all replicas will eventually converge to the same value. There’s a window during which replicas may return different values.
Where eventual consistency is fine:
- Social media feed (10ms of lag between user posts is imperceptible)
- Product catalog (price changes propagate within seconds — acceptable)
- User preferences / settings (slight delay in reflecting saved settings is fine)
- Shopping cart read (showing a slightly stale version on render is fine; write always goes to the authoritative store)
- View counts, like counts, recommendations
Where eventual consistency is dangerous:
- Bank balance (two concurrent reads could both show sufficient balance, leading to double-spend)
- Inventory reservation (two requests could both see 1 item available and both succeed)
- Authentication tokens (revoked token should not be usable after revocation)
- Order fulfillment (committing to fulfill an order requires accurate inventory state)
The pattern: eventual consistency is fine for reads of data that isn’t used as a gate on a consequential write. As soon as the read determines whether to allow a write (inventory check → place order), you need a stronger guarantee.
A specific consistency requirement that comes up constantly: after a user writes data, they should see their own write when they read.
The failure mode: User updates their profile picture. They refresh — and see the old picture. The read went to a replica that hasn’t caught up yet. User thinks the save failed; they click save again. Race conditions ensue.
How to achieve it:
- Route reads after write to the primary. Simple. Adds latency (primary may be farther away).
- Track the write’s replication token and only serve the read from a replica that has caught up to that token. DynamoDB and some Postgres drivers support this.
- Read your own writes via the cache. After writing, update the cache. Reads go to cache first. TTL ensures eventual fallback to replica.
- Client-side state. Don’t re-fetch after write — update the local state optimistically. User sees their write immediately because the client renders it; the replica discrepancy is irrelevant.
Strong (linearizable) consistency means a read always returns the most recent committed write. Every reader sees a consistent, global ordering of operations.
When it’s worth the latency and complexity:
- Financial transactions — account balance, ledger entries
- Inventory management — decrement stock only if available
- Distributed locking — only one holder at a time
- Seat reservations, ticket booking — no double-booking
- Authentication / authorization state — revoked tokens must not grant access
The implementation question: How do you achieve it? Options:
- Route to primary — simplest, the primary is authoritative
- Quorum reads — read from majority of replicas (Cassandra
QUORUM, DynamoDB strong reads) - Serializable isolation — full serializable transaction isolation in Postgres
- Optimistic locking — read a version number, write only if version matches, retry on conflict
Bank Balance:
- Reads must be strongly consistent — you’re making a decision (can I withdraw?) based on this read
- Writes must be atomic and durable
- Consistency model: serializable transactions on the ledger
- Availability trade-off: it’s acceptable to return an error rather than a stale balance
- Implementation: transactions against a single authoritative database; replicas for reporting only
Social Feed:
- Reads can be eventually consistent — 50ms of lag in feed updates is imperceptible
- High write throughput (millions of posts/second globally)
- Consistency model: eventual, with monotonic reads (you don’t see posts disappear after you’ve seen them)
- Availability trade-off: it’s better to show a slightly stale feed than to return an error
- Implementation: fan-out on write (push to follower timelines) or fan-out on read (pull and merge), Cassandra or Redis for timeline storage, CDN caching for popular feeds
The honest, non-technical explanation:
“When our database servers can’t talk to each other (a network split), we have a choice: do we keep accepting writes and reads (availability), or do we refuse operations until we know all servers agree on the current data (consistency)?
For most of our features — feed, search, recommendations — it’s fine if different users see slightly different results for a few seconds. We prioritize availability.
For payments and inventory, we cannot show you a balance that’s even 1 cent wrong. We prioritize consistency, and we’ll return an error rather than give you incorrect data.”
Then anchor it to the product: “This is why the checkout flow sometimes shows an ‘out of stock’ error even after you saw 1 item available — the inventory check happened at a different moment, and we’d rather give you a correct error than charge you for something we can’t fulfill.”