Consistency Trade-offs
The CAP/PACELC choice between strong and eventual consistency, and how to pick per data type.
Why
Strong consistency (linearizable reads) is simple to reason about but limits availability and latency under partitions. Eventual consistency is fast and always-available but lets reads see stale data, which is a bug if you assumed freshness. The trade-off isn't global — it's per data type: a bank balance needs strong consistency (overdraft is a legal issue), a view counter tolerates eventual (a 2-second stale count is fine). Making the choice explicit per data type prevents the two failure modes: over-requiring strong consistency (slow, unavailable) and under-requiring it (stale-data bugs).
When
During data modeling, per data type. Takes 30 min per type. Revisit if staleness bugs or availability issues appear.
How
1. For each data type, ask: "what's the harm if a read sees 2-second-old data?" - No harm (view count, leaderboard) → eventual consistency OK - Annoying (profile update) → eventual with short lag (1s) OK - Harmful (balance, inventory, permission) → strong consistency required 2. For strong-consistency data, use a single-writer (DB primary) and linearizable reads 3. For eventual data, use read replicas / async replication with a documented staleness bound 4. If you mix, label each read with its consistency level so callers know what they're getting 5. Test the eventual paths: inject a 2s replication lag and confirm the app still behaves correctly
Example
Data types and consistency:
account_balance → STRONG (overdraft = legal issue) — read from primary
product_inventory → STRONG (oversell = chargeback) — read from primary, lock on write
user_profile → EVENTUAL, <1s lag — read from replica
view_count → EVENTUAL, <5s lag — read from replica, batched increments
search_index → EVENTUAL, <30s lag — async reindex, acceptable
Mixed read endpoint:
GET /v1/account → { balance (strong), profile (eventual, ~0.5s stale) }Common Problems
- ⚠Defaulting to strong consistency for everything — the system is slow and unavailable when a replica lags, for data that didn't need freshness.
Best Practices
- ✓Document the staleness bound for every eventually-consistent read — 'eventually' is not a spec; '<2s' is.
- ✓Put the consistency choice in the data model doc so a future engineer doesn't 'fix' a strong-consistency requirement by adding a cache.