Loading learning content...
You've mastered the theory of CAP—consistency, availability, and partition tolerance. You understand the formal definitions, the trade-offs, and the impossibility results. But sitting in a design review, someone asks: "Should we use Cassandra or PostgreSQL for this service?" Theory alone won't answer that question.
CAP in practice is about translating theoretical understanding into architectural decisions. It's about recognizing that real systems exist on a spectrum, not in neat categories. It's about understanding that the same system can behave as CP in one configuration and AP in another. And it's about knowing when CAP is the right framework and when other considerations dominate.
By the end of this page, you will have practical frameworks for making CAP-informed decisions, understand how popular distributed systems make their CAP trade-offs, know how to evaluate systems against your requirements, and develop intuition for when CAP is the primary concern versus when other factors dominate.
When designing a distributed system, use this framework to make CAP-informed decisions:
Step 1: Identify Your Data Categories
Not all data has the same requirements. Categorize your data:
| Category | Examples | Typical Priority |
|---|---|---|
| Financial | Payments, balances, ledgers | Consistency |
| User Identity | Authentication, permissions | Consistency |
| User Content | Posts, comments, uploads | Often availability |
| Session Data | Login state, preferences | Availability |
| Analytics | Metrics, logs, events | Availability |
| Inventory | Stock levels, reservations | Context-dependent |
| Catalog | Products, prices, descriptions | Often availability |
Step 2: Define Your Failure Scenarios
For each category, ask: What happens during a partition?
Step 3: Choose Your Strategy Per Category
For data requiring consistency:
For data tolerating eventual consistency:
For data that needs both (the hard cases):
Step 4: Validate with Failure Testing
After implementation, validate your choices:
In most systems, 80% of data can use a simple AP strategy with eventual consistency. Only 20% (or less) truly requires strong consistency. Don't over-engineer. Start with the question: 'What's the actual damage if this data is briefly inconsistent?' Often, the answer is 'not much.'
Understanding how production systems handle CAP provides practical insight. Let's examine several popular distributed databases and their CAP characteristics.
| System | CAP Mode | Consistency Model | Partition Behavior | When to Use |
|---|---|---|---|---|
| PostgreSQL (single) | CA* | Strong (ACID) | N/A (single node) | Strong consistency, moderate scale |
| PostgreSQL (sync rep) | CP | Strong | Minority side unavailable | Strong consistency with HA |
| MySQL Cluster | CP | Strong | Waits for majority | OLTP with high availability |
| CockroachDB | CP | Serializable | Minority partitions readonly | Distributed SQL, strong consistency |
| MongoDB | Configurable | Tunable per-op | Depends on write/read concern | Flexible, document-oriented |
| Cassandra | AP | Eventual | All nodes serve | High availability, write-heavy |
| DynamoDB | AP | Eventual (default) | Continues operating | Serverless, predictable performance |
| Redis Cluster | CP (consensus) | Strong within slot | Minority unavailable | Caching with availability |
| etcd | CP | Linearizable | Minority unavailable | Configuration, coordination |
| ZooKeeper | CP | Linearizable | Minority unavailable | Coordination, leader election |
Labeling a system 'CP' or 'AP' oversimplifies. Most modern systems offer configurable consistency. MongoDB can be CP or AP depending on writeConcern and readConcern settings. DynamoDB offers strong consistency reads. The label describes default or primary behavior, not absolute constraints.
Deep Dive: MongoDB's Configurable Consistency
MongoDB beautifully illustrates how modern systems offer tunable consistency:
Write Concern: Controls durability/consistency of writes
w: 1 - Acknowledged when primary receives (AP-ish)w: majority - Acknowledged when majority has it (CP-ish)w: 0 - Fire and forget (pure AP)Read Concern: Controls consistency of reads
local - Returns primary's data, may be rolled back (AP)majority - Returns data confirmed by majority (CP)linearizable - Reflects all acknowledged writes (strong CP)Read Preference: Where to read from
primary - Always read from primary (consistent but SPOF)secondary - Read from secondaries (available but potentially stale)nearest - Read from lowest-latency node (fastest but unpredictable consistency)This flexibility lets you tune per-operation, using strong consistency for critical ops and relaxed consistency for others.
CP systems sacrifice availability during partitions to maintain consistency. Let's explore when and how to use them effectively.
When to Choose CP:
Implementing CP Effectively:
1. Use consensus-based systems (etcd, ZooKeeper, Consul)
2. PostgreSQL with synchronous replication
synchronous_commit = on
synchronous_standby_names = 'first 1 (standby1, standby2)'
Writes are acknowledged only after at least one standby confirms. Partition of primary from all standbys means writes block.
3. CockroachDB for distributed SQL
12345678910111213141516171819202122232425262728293031
SCENARIO: Distributed lock for critical section protection Using etcd or ZooKeeper for CP lock: ┌─────────────────────────────────────────────────────────────────┐│ 1. Client acquires lock ││ PUT /locks/resource-123 ││ Value: client-id, TTL: 30s ││ Response: { "lease_id": 12345, "fencing_token": 42 } ││ ││ 2. Client performs work, includes fencing token ││ UPDATE resources SET ... WHERE fencing_token < 42 ││ (Only succeeds if we're the latest lock holder) ││ ││ 3. Client releases lock (or TTL expires) ││ DELETE /locks/resource-123 │└─────────────────────────────────────────────────────────────────┘ WHY FENCING TOKENS MATTER:Without fencing: - Client A gets lock, then pauses (GC, network delay) - Lock TTL expires, Client B gets lock - Client A resumes, thinks it has lock - Both A and B modify resource → Corruption! With fencing: - Client A: fencing_token = 42 - Client B: fencing_token = 43 - A's delayed write includes token 42 - Storage rejects: token 42 < current token 43 - Only B's writes succeed → Correctness preserved!Trade-offs of CP Systems:
Latency: Writes go through consensus, adding round-trips. Expect 10-100ms per write depending on deployment topology.
Availability during partitions: Minority partitions are completely unavailable. In a 5-node cluster, 2 failing nodes means the minority cannot serve requests.
Scalability challenges: Every write must be agreed upon, limiting write throughput. Many CP systems don't scale writes horizontally well.
Complexity: Consensus protocols are subtle. Many implementations have had bugs. Use well-tested systems.
Mitigation strategies:
CP systems guarantee consistency when they respond, but they may not respond during partitions. A CP system with all nodes in one datacenter that loses internet connectivity is 100% unavailable externally. CP is about choosing unavailability over inconsistency—but the unavailability is real.
AP systems continue serving requests during partitions, accepting that replicas may temporarily diverge. Let's explore when and how to use them effectively.
When to Choose AP:
Implementing AP Effectively:
1. Choose the right conflict resolution strategy
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Last-Writer-Wins (LWW) | Latest timestamp wins | Simple, automatic | Silently loses data |
| Multi-Value | Return all versions | No data loss | Pushes resolution to app |
| CRDTs | Mathematically merge | Automatic, no loss (for supported types) | Limited data type support |
| Custom merge | Application logic | Full control | Complex, error-prone |
123456789101112131415161718192021222324252627282930
SCENARIO: Shopping cart across partitioned regions Traditional approach (problematic):┌─────────────────────────────────────────────────────────────────┐│ Partition A: cart = ["item1"] ││ Partition B: cart = ["item2"] ││ ││ After merge with LWW: cart = ["item2"] (item1 LOST!) │└─────────────────────────────────────────────────────────────────┘ CRDT approach (G-Set: Grow-only Set)┌─────────────────────────────────────────────────────────────────┐│ Partition A: cart = {"item1": {added: true}} ││ Partition B: cart = {"item2": {added: true}} ││ ││ Merge operation: UNION of sets ││ After merge: cart = {"item1": ..., "item2": ...} (both items!) │└─────────────────────────────────────────────────────────────────┘ For removals, use OR-Set (Observed-Remove Set):┌─────────────────────────────────────────────────────────────────┐│ Each item has unique ID + operation tag ││ ││ add("apple", id=1) → {apple: {adds: [{id:1}], removes: []}} ││ add("banana", id=2) → {banana: {adds: [{id:2}], removes: []}}││ remove("apple", id=1)→ {apple: {adds: [{id:1}], removes:[1]}} ││ ││ Item visible if ANY add not in removes ││ Concurrent add and remove: add wins (user will remove again) │└─────────────────────────────────────────────────────────────────┘2. Implement read-your-writes when possible
Even in AP systems, users expect to see their own changes. Strategies:
3. Monitor and alert on divergence
AP systems should track:
Alert when divergence exceeds thresholds—a replica that's hours behind may have a problem.
When designing AP systems, start by designing the reconciliation process. How will you detect conflicts? How will you resolve them? What's the user experience during reconciliation? If you can't answer these questions, you're not ready to deploy AP.
Modern distributed systems increasingly offer tunable consistency, allowing per-operation or per-table consistency settings. This is powerful but introduces complexity.
How Tunable Consistency Works:
Most tunable systems use quorum-based logic:
Guarantee: If W + R > N, at least one node in the read quorum was in the write quorum, ensuring you read the latest write.
Examples configurations for N=3:
| Setting | W | R | W+R | Behavior |
|---|---|---|---|---|
| Strong | 2 | 2 | 4 | Consistent: any read sees latest write |
| Write-optimized | 1 | 3 | 4 | Fast writes, slow reads, still consistent |
| Read-optimized | 3 | 1 | 4 | Slow writes, fast reads, still consistent |
| Eventual | 1 | 1 | 2 | Fast but may read stale data |
1234567891011121314151617181920212223242526272829
Cassandra Consistency Levels with RF=3 (3 replicas): WRITE CONSISTENCY LEVELS:┌────────────────────────────────────────────────────────────────┐│ ONE │ Write to 1 node, return. Fast, risky. ││ QUORUM │ Write to 2/3 nodes. Balanced. ││ ALL │ Write to 3/3 nodes. Slow, safest. ││ LOCAL_QUORUM │ Quorum within local datacenter. ││ EACH_QUORUM │ Quorum in each datacenter. For multi-DC. │└────────────────────────────────────────────────────────────────┘ READ CONSISTENCY LEVELS:┌────────────────────────────────────────────────────────────────┐│ ONE │ Read from nearest. Fastest, may be stale. ││ QUORUM │ Read from 2/3, return newest. Consistent. ││ ALL │ Read from 3/3. Slowest, most consistent. ││ LOCAL_QUORUM │ Quorum within local datacenter. │└────────────────────────────────────────────────────────────────┘ CONSISTENCY COMBINATIONS:┌────────────────────────────────────────────────────────────────┐│ For Strong Consistency: ││ W=QUORUM + R=QUORUM → W(2) + R(2) = 4 > N(3) ✓ ││ W=ALL + R=ONE → W(3) + R(1) = 4 > N(3) ✓ ││ ││ For Eventual Consistency (fast): ││ W=ONE + R=ONE → W(1) + R(1) = 2 < N(3) ✗ ││ May read stale data, but very fast! │└────────────────────────────────────────────────────────────────┘Practical Application: Per-Operation Tuning
Develop conventions for your application:
Critical operations: Use strong consistency
// User signup - must not duplicate
await cassandra.execute(
'INSERT INTO users ...',
values,
{ consistency: types.consistencies.quorum }
);
Read-heavy analytics: Use eventual consistency
// Dashboard view - okay if slightly stale
await cassandra.execute(
'SELECT * FROM page_views ...',
values,
{ consistency: types.consistencies.one }
);
The Danger of Tunable Consistency:
Tunable consistency adds cognitive load and error opportunity:
Best practice: Establish a small set of allowed patterns (e.g., "critical writes" and "casual reads") and use framework-level enforcement. Don't let every developer set consistency ad-hoc.
Higher consistency levels have direct performance costs. QUORUM writes are ~2x the latency of ONE writes. ALL reads are 3x the latency of ONE reads (with RF=3). Profile your application to understand the actual trade-offs in your deployment.
CAP is foundational, but it's not the only framework for distributed system trade-offs. Understanding related concepts provides a more complete picture.
PACELC: Extending CAP
The PACELC theorem extends CAP to describe behavior when there is NO partition:
Ppartition → Availability/Consistency Else (no partition) → Latency/Consistency
Translation:
This captures an important reality: even without partitions, synchronous replication (strong consistency) increases latency. Some systems sacrifice consistency for lower latency even when they could achieve consistency.
Examples in PACELC terms:
| System | Partition (A/C) | Else (L/C) | Description |
|---|---|---|---|
| PostgreSQL (sync) | C | C | Always consistent, pays latency cost |
| Cassandra (default) | A | L | Available and fast, eventual consistency |
| DynamoDB (default) | A | L | Available and fast, eventual consistency |
| DynamoDB (strong read) | A | C | Available, but pays latency for consistency |
| VoltDB | C | C | Strongly consistent, accepts higher latency |
| PNUTS (Yahoo) | A | L | Prioritized availability and latency |
ACID vs BASE
Database consistency is often discussed in terms of ACID (traditional) vs BASE (NoSQL):
ACID:
BASE:
BASE systems trade ACID guarantees for availability and partition tolerance. They're not "worse"—they're optimized for different use cases.
Consistency Models Hierarchy:
Beyond CAP's binary consistency, there's a rich hierarchy:
CAP, PACELC, ACID, BASE—these are all tools for reasoning about distributed systems. CAP helps when thinking about partition behavior. PACELC adds latency considerations. ACID/BASE helps compare transactional guarantees. Use whichever framework is most illuminating for your specific decision.
Let's examine architectural patterns that apply CAP thinking to real systems.
Pattern 1: Polyglot Persistence
Use different databases for different data based on CAP requirements:
┌─────────────────────────────────────────────────────────────┐
│ MICROSERVICES SYSTEM │
├─────────────────────────────────────────────────────────────┤
│ Order Service → PostgreSQL (CP) │
│ (Financial data needs strong consistency) │
│ │
│ Session Service → Redis Cluster (AP) │
│ (Sessions need availability, can tolerate staleness) │
│ │
│ Product Catalog → Elasticsearch (AP) │
│ (Search needs availability, eventual updates are fine) │
│ │
│ User Preferences → DynamoDB (AP) │
│ (Preferences can be eventually consistent) │
│ │
│ Configuration → etcd (CP) │
│ (Config must be consistent across all services) │
└─────────────────────────────────────────────────────────────┘
Pattern 2: CQRS with Event Sourcing
Command Query Responsibility Segregation separates reads from writes, allowing different consistency for each:
12345678910111213141516171819202122232425262728293031323334353637
PROBLEM: Order placement needs to:1. Reserve inventory (Inventory Service)2. Process payment (Payment Service) 3. Confirm order (Order Service) Can't use distributed transactions (2PC) - too slow, hurts availability. SAGA PATTERN SOLUTION:┌─────────────────────────────────────────────────────────────────┐│ CHOREOGRAPHY SAGA: ││ ││ 1. Order Service: Create order (PENDING) ││ → Publish: OrderCreated event ││ ││ 2. Inventory Service: Receives OrderCreated ││ → Reserve inventory ││ → Publish: InventoryReserved event ││ ││ 3. Payment Service: Receives InventoryReserved ││ → Process payment ││ → Publish: PaymentProcessed event ││ ││ 4. Order Service: Receives PaymentProcessed ││ → Mark order CONFIRMED │└─────────────────────────────────────────────────────────────────┘ COMPENSATION ON FAILURE:┌─────────────────────────────────────────────────────────────────┐│ If Payment fails: ││ → Payment Service: Publish PaymentFailed ││ → Inventory Service: Release reserved inventory ││ → Order Service: Mark order CANCELLED ││ ││ Result: Eventually consistent, but correct! ││ Trade-off: Temporary inconsistency (order PENDING but no ││ actual reservation in payment failure window) │└─────────────────────────────────────────────────────────────────┘Pattern 3: Cache-Aside with TTL
In this pattern, the cache (AP) provides low-latency reads while the database (CP) remains the source of truth:
Trade-off: Reads may return stale data up to TTL duration. Acceptable for many use cases (product pages, user profiles, analytics dashboards).
Pattern 4: Write-Ahead Log with Async Replication
Local writes are durable to a WAL, then asynchronously replicated:
Trade-off: If the local node fails before replication, data is lost from other nodes' perspective. But writes are fast and available.
Real systems combine multiple patterns. An e-commerce platform might use Polyglot Persistence for storage selection, CQRS for order processing, Saga for distributed transactions, and Cache-Aside for catalog reads. Each pattern addresses a specific CAP challenge in a specific component.
We've bridged theory and practice, exploring how CAP principles translate into real architectural decisions. Let's consolidate the essential insights.
What's Next:
With CAP in Practice covered, we'll close this module with Choosing Between CP and AP—a focused decision guide for making the final call. You'll get concrete criteria, decision trees, and case studies to solidify your ability to make CAP-informed architectural choices.
You now understand how to apply CAP theory in practice: decision frameworks, how popular systems make trade-offs, implementing CP and AP strategies, tunable consistency, and architectural patterns. This practical knowledge transforms CAP from an abstract theorem into a daily design tool.