CAP Theorem & Distributed Systems: The Complete Guide
A Senior+ Guide — Consistency, Availability, Partition Tolerance & Beyond
1. Introduction: Why CAP Theorem Matters
The CAP theorem is the single most important theoretical result in distributed systems. First conjectured by Eric Brewer in 2000 and formally proved by Gilbert and Lynch in 2002, it establishes a fundamental limit on what any distributed data store can guarantee. Understanding this theorem is not an academic exercise — it directly determines the architecture, technology choices, and operational behavior of every distributed system you will ever build or operate.
In a single-machine system, consistency and availability are trivially achievable. A process reads from memory, gets the latest write, and responds instantly. The moment you replicate that data across multiple machines connected by a network, everything changes. Network messages can be lost, delayed, or duplicated. Machines can crash at any moment. Clocks drift. The CAP theorem tells us that when the network partitions — and it will partition — we must choose between two properties that most developers assume are free: consistency and availability.
The practical implications are enormous. If you are building a financial trading system, you might choose consistency over availability — rejecting trades during a partition is better than executing trades on stale data. If you are building a social media feed, you might choose availability over consistency — showing slightly stale data is acceptable, but returning an error is not. Every distributed system designer faces this trade-off, whether they realize it or not. The systems that ignore CAP make the trade-off unconsciously, often with disastrous results.
This guide is a comprehensive deep dive into the CAP theorem and its surrounding ecosystem of consistency models, consensus protocols, distributed transactions, and practical engineering strategies. We will move from formal theory to production-grade C# implementations, from textbook diagrams to real-world case studies from Google, Amazon, Microsoft, and Meta. By the end, you will understand not just what CAP says, but how to make informed architectural decisions that respect its constraints while serving your specific use case.
What You Will Learn
- The formal definition of CAP and why the original informal statement is misleading
- Why partition tolerance is non-negotiable and what "choosing between C and A" really means
- Strong consistency, linearizability, causal consistency, and eventual consistency — the full spectrum
- How Paxos and Raft provide consensus despite failures
- Two-phase commit, sagas, and why distributed transactions are so expensive
- CRDTs and how they enable automatic conflict resolution without coordination
- The PACELC theorem and why it matters even when your network is healthy
- Production patterns from real systems: Spanner, DynamoDB, Cassandra, CockroachDB, and more
A Brief History
Eric Brewer presented the CAP conjecture at the ACM Symposium on Principles of Distributed Computing in 2000. At the time, it was a bold claim: no proof existed. Two years later, Seth Gilbert and Nancy Lynch published a formal proof using the I/O automata model, confirming that the conjecture was indeed a theorem. The proof defines consistency as linearizability (the strongest single-object consistency model), availability as every non-failing node returning a response, and partition tolerance as the ability to operate despite arbitrary message loss between network nodes. The proof constructs a specific scenario — a simple register read/write system with two nodes — and shows that during a network partition, you cannot satisfy all three properties simultaneously.
The proof is surprisingly simple once you see the construction. Two nodes, A and B, replicate a single register. Client 1 writes value v1 to node A. Client 2 writes value v2 to node B. A network partition separates A from B. Both writes must succeed for availability. After the partition heals, both nodes must agree on a single value for consistency. But both nodes received different writes during the partition. If both writes succeeded (availability), the nodes have divergent states and cannot agree on the "correct" value without additional information (breaking consistency). If one write is rejected (sacrificing availability), consistency is preserved. This is the essence of CAP.
Real-World Relevance
| Industry | CAP Choice | Reason | Example System |
|---|---|---|---|
| Finance | CP (Consistency over Availability) | Stale data leads to incorrect trading decisions | Google Spanner, CockroachDB |
| Social Media | AP (Availability over Consistency) | Showing stale posts is acceptable; errors are not | Cassandra, DynamoDB |
| E-Commerce Inventory | CP with tunable reads | Over-selling is expensive; read-your-writes for sellers | Amazon Aurora, YugabyteDB |
| IoT Telemetry | AP (eventual) | High write throughput; stale reads are fine | InfluxDB, ScyllaDB |
| User Authentication | CP (strong) | Must verify credentials against latest state | etcd, ZooKeeper |
2. Formal Definition of CAP Theorem
The CAP theorem is formally defined using three properties applied to a replicated data store with two or more nodes. Understanding the precise definitions is critical because the common shorthand — "pick two of three" — is a dangerous oversimplification that leads to bad architectural decisions.
Consistency (Linearizability)
In the formal CAP definition, consistency means linearizability. This is the strongest single-object consistency model. Every read returns the result of the most recent completed write. All operations appear to execute atomically in some total order that is consistent with the real-time ordering of operations. If write W completes before read R starts, R must return the value written by W (or a later value). This is a strict guarantee — no stale reads, no partial views, no anomalies.
Linearizability is stronger than sequential consistency, which is stronger than causal consistency, which is stronger than eventual consistency. The formal proof specifically uses linearizability, which means the CAP theorem applies most forcefully to systems requiring the strongest form of consistency. Systems that relax consistency requirements (to eventual, causal, etc.) have more design freedom.
Availability
Formally, availability means: every request to a non-failing node must receive a response. Not a timeout, not an error — a valid response containing data. This does not mean every request succeeds immediately; it means every request eventually receives a non-error response as long as the node itself has not failed. The critical nuance is that the response must be meaningful. A system that returns "sorry, I can't help you" during a partition is technically unavailable, even if it is responding quickly.
Partition Tolerance
A network partition is when messages between two groups of nodes are arbitrarily delayed or lost. In a distributed system, partitions are not exceptional — they are a routine operational reality. Hardware failures, network congestion, configuration errors, and even scheduled maintenance can all cause partitions. The formal definition requires that the system continues to operate correctly despite arbitrary message loss between any pair of nodes. Since we cannot prevent partitions (they are caused by the physical world), partition tolerance is effectively a constraint, not a choice.
The Proof in Plain Language
Consider two nodes, N1 and N2, replicating a single variable x. Initially, x = 0 on both nodes. A network partition separates N1 from N2. Client 1 writes x = 1 to N1. Client 2 reads x from N2. For N2 to be available, it must respond to the read. But N2 cannot contact N1, so it does not know about the write. If N2 returns x = 0, the read is inconsistent (the write of x = 1 is already "done" in the system). If N2 returns x = 1, it must have received the write somehow, contradicting the partition. If N2 refuses to respond, it is not available. This is the CAP trilemma in its simplest form.
C#
// Formal CAP constraint illustrated as a simple distributed register
public class DistributedRegister
{
private readonly INode _primary;
private readonly INode _secondary;
private readonly INetworkPartitionDetector _partitionDetector;
public async Task<bool> WriteAsync(string key, string value)
{
// CAP: During partition, we must choose:
if (await _partitionDetector.IsPartitionedAsync())
{
// CP: Reject the write to maintain consistency
throw new PartitionException(
"Write rejected: network partition detected");
}
// AP: Accept writes on both sides (may diverge)
await _primary.SetAsync(key, value);
return true;
}
public async Task<string> ReadAsync(string key)
{
if (await _partitionDetector.IsPartitionedAsync())
{
// CP: Return error or stale data (can't guarantee consistency)
// AP: Return whatever this node has (may be stale)
return await _primary.GetAsync(key);
}
// No partition: read from primary for consistency
return await _primary.GetAsync(key);
}
}
Why "Pick Two of Three" Is Wrong
The popular "pick two of three" meme is misleading because it implies all three properties are equally available choices. In reality, partition tolerance is mandatory in any distributed system. You cannot choose to "not have partitions" — the network will partition regardless of your wishes. The real choice is: during a partition, do you sacrifice consistency or availability? This is a binary choice, not a ternary one. The correct framing is: "Given that partitions will happen, do you prefer consistency or availability during those partitions?"
This framing has profound implications for system design. It means a CA system (consistent and available) can only exist in a single-node deployment or a system with perfect network connectivity, which is physically impossible at scale. Every distributed database, every microservice architecture, every replicated cache faces the CAP trade-off. The question is not whether you face it, but how consciously you make the choice.
3. Consistency Models: Strong to Eventual
Consistency is not binary — it exists on a rich spectrum from linearizability (the strongest) to eventual consistency (the weakest). Understanding this spectrum is essential because choosing the right consistency model for each component of your system is one of the most impactful architectural decisions you can make. Stronger consistency provides better guarantees but costs more in terms of latency, throughput, and availability. Weaker consistency enables higher performance and availability but pushes complexity to the application layer.
Linearizability (Strong Consistency)
Linearizability is the gold standard. Every operation appears to take effect instantaneously at some point between its invocation and its response. All nodes agree on the same total order of operations. Once a write completes, every subsequent read from any node returns that value or a later one. This is the CAP theorem's definition of consistency. Systems that provide linearizability include Google Spanner (using TrueTime), etcd (using Raft), and ZooKeeper (using ZAB). The cost is significant: every write requires coordination across a majority of nodes, adding at least one network round trip of latency.
C#
// Linearizability guarantee: after Write completes, all subsequent reads see it
public class LinearizableStore<T>
{
private readonly IRaftConsensus _raft;
private readonly ConcurrentDictionary<string, T> _localCache = new();
public async Task WriteAsync(string key, T value)
{
// Write must be replicated to a majority before returning
var entry = new LogEntry { Key = key, Value = value,
Timestamp = Timestamp.UtcNow() };
await _raft.ReplicateAsync(entry); // Blocks until majority acknowledges
_localCache[key] = value;
}
public async Task<T> ReadAsync(string key)
{
// Read from the leader's committed state
// or perform a read index check for followers
if (_localCache.TryGetValue(key, out var cached))
return cached;
return await _raft.ReadCommittedAsync(key);
}
}
Sequential Consistency
Sequential consistency is weaker than linearizability but stronger than causal consistency. All operations appear in some total order that is consistent with the program order of each individual process, but the order may not correspond to real-time. For example, Process 1 writes x=1 then reads y, and Process 2 reads y then writes x=2. Sequential consistency allows x to be 2 when Process 1 reads it even though Process 2's write hasn't "happened yet" in real time. The operations are interleaved in a way that preserves each process's order, but not global real-time order.
Causal Consistency
Causal consistency preserves the order of causally related operations but allows concurrent operations to be seen in different orders by different nodes. If write A causes write B (B depends on the result of A), every node will see A before B. But if two writes are concurrent (neither caused the other), nodes may see them in different orders. This is the sweet spot for many applications — it prevents the most confusing anomalies (reading your own writes, seeing effects before causes) while allowing much higher performance than linearizability.
C#
// Causal consistency using vector clocks
public class CausalRegister
{
private readonly Dictionary<string, (string Value, VectorClock Clock)> _data = new();
public void Write(string key, string value, VectorClock clientClock)
{
if (_data.TryGetValue(key, out var existing))
{
var merged = VectorClock.Merge(existing.Clock, clientClock);
merged.Increment(clientId);
_data[key] = (value, merged);
}
else
{
var clock = new VectorClock(clientId, 1);
_data[key] = (value, clock);
}
}
public string Read(string key)
{
// Return value with the latest causal timestamp
return _data.TryGetValue(key, out var entry) ? entry.Value : null;
}
}
public class VectorClock
{
private readonly Dictionary<string, int> _versions = new();
public static VectorClock Merge(VectorClock a, VectorClock b)
{
var merged = new VectorClock();
var allKeys = a._versions.Keys.Concat(b._versions.Keys).Distinct();
foreach (var key in allKeys)
{
merged._versions[key] = Math.Max(
a._versions.GetValueOrDefault(key, 0),
b._versions.GetValueOrDefault(key, 0));
}
return merged;
}
public void Increment(string nodeId)
{
_versions[nodeId] = _versions.GetValueOrDefault(nodeId, 0) + 1;
}
}
Eventual Consistency
Eventual consistency is the weakest guarantee: if no new writes are made, all replicas will eventually converge to the same value. "Eventually" is undefined — it could be milliseconds or minutes. This model enables the highest availability and lowest latency because nodes can accept writes immediately without coordinating with others. The trade-off is that reads may return stale data, and the application must be designed to tolerate this staleness. Most web applications are eventually consistent to some degree — DNS propagation, CDN cache invalidation, and database replication lag all introduce eventual consistency.
Consistency Model Comparison
| Model | Guarantee | Latency | Availability | Example System |
|---|---|---|---|---|
| Linearizability | Reads reflect latest write globally | High (cross-node round trip) | Lower during partitions | Spanner, etcd, ZooKeeper |
| Sequential | Global order consistent with program order | Moderate | Moderate | Paxos-based stores |
| Causal | Causally related ops seen in order | Low-moderate | High | MongoDB (sessions), Azure Cosmos DB |
| Read-your-writes | Reader sees their own writes | Low | High | Most web applications (session stickiness) |
| Monotonic reads | Reads never go backward in time | Low | High | DynamoDB (strongly consistent reads) |
| Eventual | Converges eventually | Minimal | Highest | Cassandra, DynamoDB (eventual reads) |
4. Availability in Distributed Systems
Availability in the CAP sense is absolute: every request to a non-failing node must receive a response. This is stricter than what most people think of as "high availability" (e.g., 99.99% uptime). CAP availability means zero failed requests from healthy nodes, even during network partitions. In practice, most systems relax this to "high availability" — accepting occasional failures while striving for minimal downtime.
Why Availability Is Hard
Achieving availability during partitions requires every node to respond independently. But if nodes cannot communicate, they cannot coordinate. Consider a distributed counter: Node A and Node B both maintain a count. During a partition, Client 1 increments on A (count = 1), and Client 2 increments on B (count = 1). Both must succeed for availability. After the partition heals, the counts diverge — A says 1, B says 1, but the true count is 2. The system must have a conflict resolution strategy: last-write-wins, additive merge, CRDT merge, or application-level resolution. Each strategy has trade-offs that affect correctness.
Availability Patterns
C#
// AP System: Accept writes on all partitions, resolve conflicts later
public class AvailableCounter
{
private readonly ILocalStore _store;
private readonly IClock _clock;
public async Task IncrementAsync(string counterId)
{
var current = await _store.GetAsync(counterId);
var newCount = (current?.Value ?? 0) + 1;
// Always accept the write — this node is available
await _store.SetAsync(counterId, new CountEntry
{
Value = newCount,
NodeId = Environment.MachineName,
Timestamp = _clock.Now()
});
// Conflict resolution happens asynchronously after partition heals
}
public async Task MergeAsync(string counterId, CountEntry remote)
{
var local = await _store.GetAsync(counterId);
if (local == null)
{
await _store.SetAsync(counterId, remote);
return;
}
// CRDT merge: take the max from each node's counter
var merged = new CountEntry
{
Value = Math.Max(local.Value, remote.Value),
NodeId = "merged",
Timestamp = Timestamp.UtcNow()
};
await _store.SetAsync(counterId, merged);
}
}
Health Checks and Load Balancing
Availability extends beyond CAP — it encompasses health checking, load balancing, and graceful degradation. Systems use multiple strategies to maintain availability: load balancers that route around unhealthy nodes, circuit breakers that prevent cascading failures, retries with exponential backoff for transient errors, and fallback responses when primary services are unavailable. These patterns do not violate CAP because they operate above the storage layer and accept temporary consistency or data freshness trade-offs.
C#
// Circuit breaker pattern for availability during partial failures
public class ResilientDistributedClient
{
private readonly CircuitBreaker _circuitBreaker;
private readonly DistributedStore _store;
public async Task<Data> ReadAsync(string key)
{
if (_circuitBreaker.State == CircuitState.Open)
{
// Serve from local cache during failure — available but potentially stale
return await GetFromLocalCacheAsync(key);
}
try
{
var result = await _store.ReadAsync(key);
_circuitBreaker.RecordSuccess();
await UpdateLocalCacheAsync(key, result);
return result;
}
catch (Exception)
{
_circuitBreaker.RecordFailure();
return await GetFromLocalCacheAsync(key);
}
}
}
5. Partition Tolerance: Why It Is Non-Negotiable
Partition tolerance is not optional in distributed systems. Network partitions are caused by physical realities: hardware failures, network congestion, cable cuts, software bugs in network stacks, misconfigured firewalls, and even cosmic rays flipping bits in routers. At the scale of data centers with thousands of machines, partitions happen daily. Google has reported network partitions occurring multiple times per year across their global infrastructure. Amazon has documented partition events caused by configuration changes that isolated entire availability zones.
Types of Partitions
Not all partitions are created equal. A complete partition (all messages between two node groups are lost) is the most severe but also the easiest to reason about. Partial partitions (some messages get through, others are lost) are far more dangerous because they create inconsistent views of the system state. Asymmetric partitions (messages flow from A to B but not from B to A) are particularly insidious because node A believes it is communicating normally while B is unreachable. These "gray failures" are the hardest to detect and handle correctly.
Handling Partitions in Practice
C#
// Partition detection and response strategy
public class PartitionAwareNode
{
private readonly IClusterMembership _membership;
private readonly IHealthProbe _peerProbes;
private readonly TimeSpan _partitionTimeout = TimeSpan.FromSeconds(5);
public async Task<PartitionState> DetectPartitionAsync()
{
var unreachablePeers = new List<string>();
var healthyPeers = new List<string>();
foreach (var peer in await _membership.GetPeersAsync())
{
var latency = await _peerProbes.MeasureLatencyAsync(
peer, _partitionTimeout);
if (latency == null)
unreachablePeers.Add(peer.Id);
else
healthyPeers.Add(peer.Id);
}
if (unreachablePeers.Count == 0)
return PartitionState.None;
// Check if we can reach a majority (quorum check)
var totalNodes = healthyPeers.Count + unreachablePeers.Count;
var hasQuorum = healthyPeers.Count >= (totalNodes / 2) + 1;
return hasQuorum
? PartitionState.MinorityPartition // We have quorum, can continue
: PartitionState.MajorityPartition; // We are in minority, must yield
}
}
public enum PartitionState
{
None, // No partition detected
MinorityPartition, // This node is in the majority partition (can continue)
MajorityPartition // This node is in the minority (must stop accepting writes)
}
Partition Tolerance Strategies
| Strategy | Behavior During Partition | Trade-off | Best For |
|---|---|---|---|
| Majority-based quorum | Minority partition stops serving writes | Reduced availability for minority | CP systems (etcd, ZooKeeper, Spanner) |
| Last-write-wins | Both sides accept writes, converge later | May lose writes (last write overwrites) | AP systems (Cassandra, DynamoDB) |
| Conflict-free merge (CRDT) | Both sides accept writes, merge automatically | Limited data types (counters, sets, registers) | Collaborative apps, distributed counters |
| Read-repair | Reads detect and fix inconsistencies | Read latency increases | Cassandra, Riak |
| Anti-entropy | Background sync repairs divergent data | Eventual convergence, not immediate | All eventually consistent systems |
Real-World Partition Events
In 2011, Amazon experienced a network partition that isolated a significant portion of their US-East-1 region. The DynamoDB service had to make a CAP choice: continue serving reads with stale data (AP) or reject reads to maintain consistency (CP). Amazon chose availability — the service remained accessible with some stale reads for approximately 20 minutes. For most users, this was the right call: a few stale product prices are far less damaging than a complete shopping outage.
In 2017, Google published a paper describing how Spanner handles partitions. Spanner uses TrueTime (atomic clocks + GPS) to assign globally ordered timestamps. During a partition, nodes in the minority partition cannot commit new transactions (CP choice) because they cannot guarantee their timestamps are globally ordered. This means Spanner rejects writes from isolated nodes — the conservative choice for a system where consistency is paramount. The trade-off is that users in the minority partition experience downtime, but no data corruption can occur.
6. CP vs AP Systems: Real-World Classification
Every distributed data store makes a CP or AP choice during partitions. Understanding which category a system falls into — and why — is essential for choosing the right tool for your workload. The classification is not always obvious: many systems offer tunable consistency, allowing you to be CP for some operations and AP for others.
CP Systems (Consistency over Availability)
CP systems reject or timeout requests from nodes that cannot participate in a quorum during a partition. They guarantee that every response reflects the latest committed state, at the cost of unavailability for some clients during partitions. CP systems typically use consensus protocols (Raft, Paxos, ZAB) that require majority agreement before committing writes.
| System | Consensus Protocol | Language | Partition Behavior | Use Case |
|---|---|---|---|---|
| Google Spanner | Paxos + TrueTime | C++ | Minority partition rejects writes | Global financial transactions |
| etcd | Raft | Go | Minority partition returns errors | Kubernetes metadata, leader election |
| ZooKeeper | ZAB | Java | Minority partition stops serving | Configuration management, locks |
| CockroachDB | Raft (per range) | Go | Minority replicas unavailable | Distributed SQL, ACID transactions |
| YugabyteDB | Raft (per tablet) | C++ | Minority tablets unavailable | PostgreSQL-compatible distributed DB |
| Consul | Raft | Go | Minority returns errors | Service discovery, health checking |
AP Systems (Availability over Consistency)
AP systems continue to accept reads and writes from all nodes during a partition, accepting that some responses may be stale or conflicting. They use conflict resolution mechanisms to eventually converge to a consistent state. AP systems prioritize uptime and write throughput over strict consistency.
| System | Replication | Conflict Resolution | Partition Behavior | Use Case |
|---|---|---|---|---|
| Apache Cassandra | Multi-master, tunable quorum | Last-write-wins + tombstones | All nodes accept writes | Time-series, high-write workloads |
| Amazon DynamoDB | Multi-AZ replication | Last-write-wins | All nodes accept writes | E-commerce, gaming, IoT |
| Riak | Multi-master | CRDTs, sibling resolution | All nodes accept writes | Distributed caches, session stores |
| CouchDB | Multi-master | Conflict revision tree | All nodes accept writes | Offline-first applications |
| Cosmos DB | Multi-master (configurable) | Tunable per partition key range | Depends on consistency level | Global distribution, low latency |
Hybrid Systems
Modern databases increasingly offer tunable consistency, allowing you to choose CP or AP behavior per query or per table. CockroachDB defaults to serializable isolation (CP) but allows follower reads for stale but fast queries (AP-like). Cassandra defaults to eventual consistency (AP) but supports QUORUM reads/writes that guarantee strong consistency (CP-like). Cosmos DB allows you to set consistency levels from strong to eventual on a per-account basis. These hybrid systems blur the CP/AP boundary and put the trade-off decision in the hands of the application developer.
C#
// CockroachDB: choosing consistency per operation
public class HybridConsistencyClient
{
private readonly NpgsqlConnection _connection;
// CP: Strong consistency for critical operations
public async Task<AccountBalance> GetBalanceAsync(Guid accountId)
{
// Default: SERIALIZABLE isolation, read from leaseholder
using var cmd = new NpgsqlCommand(
"SELECT balance FROM accounts WHERE id = @id", _connection);
cmd.Parameters.AddWithValue("id", accountId);
var result = await cmd.ExecuteScalarAsync();
return new AccountBalance { Amount = (decimal)result };
}
// AP: Follower reads for non-critical, low-latency queries
public async Task<IReadOnlyList<Transaction>> GetRecentActivityAsync(
Guid accountId)
{
// Follower read: may be up to 4.8 seconds stale but much faster
using var cmd = new NpgsqlCommand(
"SELECT * FROM transactions WHERE account_id = @id " +
"ORDER BY created_at DESC LIMIT 50 " +
"AS OF SYSTEM TIME follower_read_timestamp()", _connection);
cmd.Parameters.AddWithValue("id", accountId);
var reader = await cmd.ExecuteReaderAsync();
// ... read results
return null; // placeholder
}
}
7. PACELC Theorem: Beyond CAP
The PACELC theorem, formulated by Daniel Abadi in 2012, extends CAP by addressing the trade-off that exists even when there is no partition. CAP only describes behavior during partitions, but systems make consistency-latency trade-offs during normal operation too. PACELC states: if there is a partition (P), choose between availability (A) and consistency (C). Else (E — the normal case), choose between latency (L) and consistency (C).
Why PACELC Matters
CAP tells you what happens during failures. PACELC tells you what happens during normal operation, which is 99.99% of the time. A system that is CP during partitions but also takes 200ms per write (because it always waits for majority confirmation) is making a different latency-consistency trade-off than a system that is CP during partitions but writes locally in 5ms during normal operation (relaxing consistency when there is no partition). Both are CP, but their normal-operation performance characteristics are very different.
PACELC Classification
| System | During Partition | Normal Operation | PACELC Label | Implication |
|---|---|---|---|---|
| Google Spanner | CP | CC (strong consistency, higher latency via TrueTime) | PC/EC | Always consistent, even at latency cost |
| Amazon DynamoDB | AP | LC (low latency, eventual consistency) | PA/EL | Optimized for speed, accepts staleness |
| Apache Cassandra | AP | LC (low latency with ONE consistency) | PA/EL | Default: speed over correctness |
| Apache Cassandra | AP | EC (strong consistency with QUORUM) | PA/EC | Tunable: choose per query |
| CockroachDB | CP | CC (serializable, waits for majority) | PC/EC | Strong consistency always |
| MongoDB | CP | LC (local reads in replica sets) | PC/EL | Strong writes, eventually consistent reads |
| Cosmos DB (Strong) | CP | CC (session/strong consistency) | PC/EC | Global strong consistency |
| Cosmos DB (Eventual) | AP | LC (eventual, multi-master) | PA/EL | Maximum throughput, minimum latency |
C#
// PACELC decision in system design
public class PacelcDecision
{
// DynamoDB model: PA/EL — Available during partition, Low latency normally
public async Task<Item> DynamoStyleReadAsync(string key)
{
// Normal operation: read from nearest replica (low latency, eventual)
// Partition: still available from any reachable replica
return await _nearestReplica.GetAsync(key);
}
// Spanner model: PC/EC — Consistent during partition, Consistent normally
public async Task<Row> SpannerStyleReadAsync(string key)
{
// Normal operation: read from leader with TrueTime (consistent, higher latency)
// Partition: reject if not in majority (consistent, unavailable for minority)
return await _leader.ReadAsync(key, Consistency.Linearizable);
}
// Cassandra tunable: PA/EC when needed, PA/EL when not
public async Task<Row> CassandraTunableReadAsync(string key,
bool requireFreshness)
{
if (requireFreshness)
{
// QUORUM read: PA/EC — consistent even during normal op
return await _cluster.ReadAsync(key, ConsistencyLevel.Quorum);
}
else
{
// ONE read: PA/EL — fast, eventual consistency
return await _cluster.ReadAsync(key, ConsistencyLevel.One);
}
}
}
Design Implications
The PACELC framework guides technology selection more precisely than CAP alone. If your application requires consistent reads with sub-5ms latency, you need a PC/EC system like Spanner — but you will pay for that consistency in infrastructure cost and write throughput. If your application needs to absorb millions of writes per second with single-digit millisecond latency and can tolerate a few seconds of staleness, a PA/EL system like DynamoDB is the right choice. Most applications mix both patterns, using strong consistency for critical paths and eventual consistency for everything else.
8. Consensus Protocols: Paxos and Raft
Consensus protocols are the foundation of CP systems. They allow a group of nodes to agree on a single value despite node failures and network partitions. The two dominant protocols are Paxos (the theoretical foundation) and Raft (the practical alternative). Both guarantee that committed values are never lost and that all nodes converge to the same state. Understanding these protocols is essential for designing or operating any system that requires strong consistency.
Paxos: The Theory
Paxos, proposed by Leslie Lamport in 1989 (published in 1998), solves the consensus problem in asynchronous networks. It operates in two phases: Phase 1 (Prepare): a proposer sends a prepare(n) message with a unique proposal number to acceptors. Acceptors respond with any previously accepted values. Phase 2 (Accept): the proposer sends an accept(n, v) message where v is the value from the highest-numbered prepare response (or any value if no previous value was accepted). A value is chosen when a majority of acceptors accept it. Paxos ensures safety (no two different values are chosen) but requires careful engineering for liveness (progress despite failures).
C#
// Simplified Paxos implementation
public class PaxosAcceptor
{
private int _promisedProposal = -1;
private (int Proposal, string Value)? _acceptedValue;
public PrepareResponse HandlePrepare(int proposalNumber)
{
if (proposalNumber > _promisedProposal)
{
_promisedProposal = proposalNumber;
return new PrepareResponse
{
Accepted = true,
PreviousProposal = _acceptedValue?.Proposal ?? -1,
PreviousValue = _acceptedValue?.Value
};
}
return new PrepareResponse { Accepted = false };
}
public AcceptResponse HandleAccept(int proposalNumber, string value)
{
if (proposalNumber >= _promisedProposal)
{
_promisedProposal = proposalNumber;
_acceptedValue = (proposalNumber, value);
return new AcceptResponse { Accepted = true };
}
return new AcceptResponse { Accepted = false };
}
}
public class PaxosProposer
{
private readonly IReadOnlyList<PaxosAcceptor> _acceptors;
private readonly int _majority;
private int _nextProposal = 0;
public async Task<string> ProposeAsync(string value)
{
while (true)
{
_nextProposal++;
var prepareResponses = new List<PrepareResponse>();
foreach (var acceptor in _acceptors)
{
var response = acceptor.HandlePrepare(_nextProposal);
if (response.Accepted)
prepareResponses.Add(response);
}
if (prepareResponses.Count < _majority)
{
await Task.Delay(10); // Back off and retry
continue;
}
// Use value from highest-numbered previous proposal, or our value
var acceptedValue = prepareResponses
.Where(r => r.PreviousValue != null)
.OrderByDescending(r => r.PreviousProposal)
.FirstOrDefault()?.PreviousValue ?? value;
var acceptResponses = new List<AcceptResponse>();
foreach (var acceptor in _acceptors)
{
var response = acceptor.HandleAccept(
_nextProposal, acceptedValue);
if (response.Accepted)
acceptResponses.Add(response);
}
if (acceptResponses.Count >= _majority)
return acceptedValue; // Value chosen!
}
}
}
Raft: The Practice
Raft, published by Diego Ongaro and John Ousterhout in 2014, was explicitly designed for understandability while providing the same safety guarantees as Multi-Paxos. Raft decomposes consensus into three sub-problems: leader election, log replication, and safety. A single leader handles all writes, replicates log entries to followers, and commits entries once a majority acknowledges them. If the leader fails, followers detect the timeout and start a new election. Raft is used in production by etcd, Consul, CockroachDB, and TiKV.
C#
// Raft consensus simulation
public class RaftNode
{
public string NodeId { get; }
private NodeState _state = NodeState.Follower;
private int _currentTerm = 0;
private string _votedFor = null;
private List<LogEntry> _log = new();
private int _commitIndex = 0;
private readonly Timer _electionTimer;
public enum NodeState { Follower, Candidate, Leader }
public RaftNode(string nodeId, IReadOnlyList<RaftNode> peers)
{
NodeId = nodeId;
_electionTimer = new Timer(
_ => StartElection(peers),
null,
randomTimeout(),
randomTimeout());
}
private void StartElection(IReadOnlyList<RaftNode> peers)
{
_state = NodeState.Candidate;
_currentTerm++;
_votedFor = NodeId;
var votesReceived = 1; // Vote for self
foreach (var peer in peers)
{
var granted = peer.HandleRequestVote(
_currentTerm, NodeId, _log.Count, LastLogTerm());
if (granted) votesReceived++;
}
if (votesReceived > (peers.Count + 1) / 2)
{
_state = NodeState.Leader;
Console.WriteLine($"{NodeId} became leader for term {_currentTerm}");
}
}
public bool HandleRequestVote(
int term, string candidateId, int lastLogIndex, int lastLogTerm)
{
if (term < _currentTerm) return false;
if (term > _currentTerm)
{
_currentTerm = term;
_state = NodeState.Follower;
_votedFor = null;
}
if (_votedFor == null || _votedFor == candidateId)
{
if (lastLogTerm > LastLogTerm() ||
(lastLogTerm == LastLogTerm() && lastLogIndex >= _log.Count))
{
_votedFor = candidateId;
return true;
}
}
return false;
}
private int LastLogTerm() =>
_log.Count > 0 ? _log.Last().Term : 0;
private int randomTimeout() =>
Random.Shared.Next(150, 300); // milliseconds
}
Raft vs Paxos Comparison
| Aspect | Paxos | Raft |
|---|---|---|
| Understandability | Notoriously difficult | Designed for clarity |
| Leader model | Leaderless (proposer) | Strong leader |
| Log structure | Complex (no explicit log) | Simple replicated log |
| Membership changes | Complex (joint consensus) | Simple (single configuration change) |
| Production implementations | Google Spanner, Chubby | etcd, Consul, CockroachDB, TiKV |
| Performance | Similar (with optimization) | Similar (leader-based may be slightly faster) |
9. Distributed Transactions: Two-Phase Commit
Two-Phase Commit (2PC) is the classic protocol for atomic distributed transactions. It ensures that either all participants commit or all abort, maintaining atomicity across multiple databases or services. While 2PC provides strong guarantees, it has significant availability and performance costs that make it impractical for many modern distributed systems.
How 2PC Works
Phase 1 (Prepare): The coordinator sends a prepare message to all participants. Each participant writes the transaction to a durable log (write-ahead log), acquires all necessary locks, and responds with "yes" (ready to commit) or "no" (cannot commit). Phase 2 (Commit/Abort): If all participants voted "yes," the coordinator sends a commit message. If any voted "no" or timed out, the coordinator sends an abort message. Participants apply the commit or release locks on abort. The critical property is that participants cannot unilaterally commit or abort — they must wait for the coordinator's decision.
C#
// Two-phase commit coordinator
public class TwoPhaseCommitCoordinator
{
private readonly IReadOnlyList<IParticipant> _participants;
private readonly ITransactionLog _txLog;
public async Task<bool> ExecuteAsync(DistributedTransaction tx)
{
// Write decision to log BEFORE sending prepare (crash recovery)
await _txLog.WriteAsync(tx.Id, TxPhase.Preparing);
// Phase 1: Prepare
var votes = new Dictionary<string, bool>();
foreach (var participant in _participants)
{
try
{
var vote = await participant.PrepareAsync(tx);
votes[participant.Id] = vote;
}
catch (TimeoutException)
{
votes[participant.Id] = false; // Timeout = abort
}
}
// Phase 2: Decide
var allApproved = votes.Values.All(v => v);
if (allApproved)
{
await _txLog.WriteAsync(tx.Id, TxPhase.Committed);
foreach (var participant in _participants)
await participant.CommitAsync(tx.Id);
return true;
}
else
{
await _txLog.WriteAsync(tx.Id, TxPhase.Aborted);
foreach (var participant in _participants)
await participant.AbortAsync(tx.Id);
return false;
}
}
}
// Participant implementation
public class TwoPhaseParticipant : IParticipant
{
private readonly ILocalStore _store;
private readonly ITransactionLog _txLog;
public async Task<bool> PrepareAsync(DistributedTransaction tx)
{
try
{
// Acquire locks and write to WAL (but don't make visible)
await _store.PrepareAsync(tx);
await _txLog.WriteAsync(tx.Id, TxPhase.PreparedLocally);
return true; // Vote: yes, I can commit
}
catch (Exception)
{
return false; // Vote: no, I cannot commit
}
}
public async Task CommitAsync(string txId)
{
// Apply changes (make visible) and release locks
await _store.CommitAsync(txId);
}
}
The Problem with 2PC
2PC has three fundamental problems. First, it is blocking: if the coordinator crashes after sending prepare messages but before sending the decision, participants hold locks indefinitely until the coordinator recovers. This directly reduces availability. Second, it has high latency: every transaction requires at least two round trips (prepare + commit) to all participants. At geographic scale, this means hundreds of milliseconds per transaction. Third, it scales poorly: the coordinator is a single point of bottleneck, and the number of participants is practically limited to a handful.
| Problem | Impact | Mitigation |
|---|---|---|
| Blocking on coordinator crash | Held locks reduce availability | Timeout-based automatic abort (risky — may abort committed transactions) |
| Two round trips per transaction | High latency (100ms+ at geographic scale) | Pipeline commit, group commit |
| Single coordinator bottleneck | Limited throughput | Coordinator sharding by transaction scope |
| No partition tolerance | Unavailable during partition | Use Saga pattern instead |
Sagas: The Modern Alternative
Sagas decompose a distributed transaction into a sequence of local transactions, each with a compensating transaction (undo action). If any step fails, the saga executes compensating transactions in reverse order to undo the completed steps. Unlike 2PC, sagas do not hold locks across service boundaries, so they are partition-tolerant and highly available. The trade-off is that sagas provide eventual consistency — there is a window where some steps have completed but the saga as a whole has not.
C#
// Saga pattern for distributed transactions
public class OrderSaga
{
private readonly IOrderService _orders;
private readonly IPaymentService _payments;
private readonly IInventoryService _inventory;
private readonly IShippingService _shipping;
public async Task<bool> ExecuteAsync(OrderRequest request)
{
var completedSteps = new List<Func<Task>>();
try
{
// Step 1: Create order
var orderId = await _orders.CreateAsync(request);
completedSteps.Add(() => _orders.CancelAsync(orderId));
// Step 2: Reserve inventory
await _inventory.ReserveAsync(orderId, request.Items);
completedSteps.Add(() => _inventory.ReleaseAsync(orderId));
// Step 3: Process payment
await _payments.ChargeAsync(orderId, request.Total);
completedSteps.Add(() => _payments.RefundAsync(orderId));
// Step 4: Schedule shipping
await _shipping.ScheduleAsync(orderId, request.Address);
return true; // Saga completed successfully
}
catch (Exception ex)
{
// Compensate in reverse order
completedSteps.Reverse();
foreach (var compensate in completedSteps)
{
try { await compensate(); }
catch { /* Log and alert — compensation failure needs manual intervention */ }
}
return false;
}
}
}
10. Eventual Consistency and Conflict Resolution
Eventual consistency is the natural consequence of the AP choice in CAP. When a system continues to accept writes during partitions, different nodes will have different data. After the partition heals, the system must reconcile these differences. The challenge is not achieving eventual consistency (it happens automatically with anti-entropy protocols) but doing so correctly — ensuring that the converged state is meaningful and no data is silently lost.
Conflict Resolution Strategies
When two nodes have different values for the same key, the system must decide which value to keep. This is the conflict resolution problem, and the choice of strategy has profound implications for data correctness.
| Strategy | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Last-Write-Wins (LWW) | Keep the value with the latest timestamp | Simple, automatic | Loses writes, clock skew issues | Time-series data, session stores |
| Vector Clocks | Track causality, detect conflicts | Detects true conflicts | Complex, storage overhead | Collaborative editing, version control |
| CRDTs | Mathematically mergeable data types | Automatic, correct merge | Limited data types | Counters, sets, registers |
| Application-Level | Custom merge function per key | Full control | Complex, error-prone | Domain-specific merge logic |
| Conflict Log | Store all conflicting versions, resolve later | Preserves all data | Requires manual resolution | High-value data (financial records) |
C#
// Last-Write-Wins conflict resolution
public class LastWriteWinsResolver<T>
{
public T Resolve(T local, T remote)
{
var localClock = GetTimestamp(local);
var remoteClock = GetTimestamp(remote);
if (remoteClock > localClock)
return remote; // Remote is newer
if (localClock > remoteClock)
return local; // Local is newer
// Tie-break: use node ID for deterministic resolution
return GetNodeId(local) > GetNodeId(remote) ? local : remote;
}
}
// Vector clock conflict detection
public class VectorClockResolver<T>
{
public MergeResult<T> Resolve(T local, T remote)
{
var localClock = GetVectorClock(local);
var remoteClock = GetVectorClock(remote);
if (VectorClock.Dominates(localClock, remoteClock))
return MergeResult<T>.Resolved(local); // Local is newer
if (VectorClock.Dominates(remoteClock, localClock))
return MergeResult<T>.Resolved(remote); // Remote is newer
// Concurrent writes: true conflict, need application-level resolution
return MergeResult<T>.Conflict(local, remote);
}
}
public class MergeResult<T>
{
public T Value { get; init; }
public bool IsConflict { get; init; }
public T ConflictValue { get; init; }
public static MergeResult<T> Resolved(T value) =>
new() { Value = value, IsConflict = false };
public static MergeResult<T> Conflict(T local, T remote) =>
new() { Value = local, IsConflict = true, ConflictValue = remote };
}
Anti-Entropy Protocols
Even with conflict resolution, replicas need mechanisms to propagate updates and repair inconsistencies. Anti-entropy protocols are background processes that continuously reconcile data across replicas. Merkle trees (used by Cassandra and DynamoDB) provide an efficient way to detect which ranges of data are inconsistent between two replicas, minimizing the amount of data that needs to be compared and transferred. Read-repair (used by Cassandra) opportunistically fixes inconsistencies during read operations by comparing responses from multiple replicas and updating stale ones.
C#
// Simplified Merkle tree for anti-entropy synchronization
public class MerkleTree
{
private readonly Dictionary<int, string> _hashByLevel = new();
public MerkleTree(IReadOnlyList<DataRange> ranges)
{
// Build tree: leaf nodes hash individual ranges,
// internal nodes hash children
BuildTree(ranges, level: 0, startIndex: 0);
}
public IReadOnlyList<DataRange> FindDifferences(MerkleTree remote)
{
var diffs = new List<DataRange>();
FindDifferencesRecursive(this, remote, 0, 0, diffs);
return diffs;
}
private void FindDifferencesRecursive(
MerkleTree local, MerkleTree remote,
int level, int index, List<DataRange> diffs)
{
if (local._hashByLevel[level] == remote._hashByLevel[level])
return; // This subtree is identical, skip it
if (level == MaxLevel)
{
diffs.Add(GetRange(index));
return;
}
// Recurse into children (left and right subtrees)
FindDifferencesRecursive(local, remote, level + 1, index * 2, diffs);
FindDifferencesRecursive(local, remote, level + 1, index * 2 + 1, diffs);
}
}
11. CRDTs: Conflict-Free Replicated Data Types
CRDTs (Conflict-Free Replicated Data Types) are data structures that can be replicated across multiple nodes and merged automatically without coordination. They guarantee strong eventual consistency: if all replicas have received the same set of updates (in any order), they will converge to the same state. CRDTs solve the conflict resolution problem at the data type level, eliminating the need for application-level merge logic.
Types of CRDTs
There are two families of CRDTs: operation-based (commutative replicated data types, or CmRDTs) and state-based (convergent replicated data types, or CvRDTs). Operation-based CRDTs require that operations are delivered exactly-once and in a causal order, which is enforced by the messaging layer. State-based CRDTs merge by sending the full state to another replica and merging using a commutative, associative, and idempotent merge function. State-based CRDTs are more practical because they work with any reliable (but not necessarily exactly-once) delivery mechanism.
C#
// G-Counter (Grow-only Counter) — a state-based CRDT
public class GCounter
{
// Each node maintains its own counter
private readonly Dictionary<string, long> _counts = new();
public string NodeId { get; }
public GCounter(string nodeId)
{
NodeId = nodeId;
}
public void Increment()
{
_counts[NodeId] = _counts.GetValueOrDefault(NodeId, 0) + 1;
}
public long Value => _counts.Values.Sum();
// Merge: take the max from each node
public void Merge(GCounter other)
{
foreach (var (node, count) in other._counts)
{
_counts[node] = Math.Max(
_counts.GetValueOrDefault(node, 0), count);
}
}
public bool Equals(GCounter other)
{
return Value == other.Value;
}
}
// PN-Counter (Positive-Negative Counter) — supports decrement
public class PNCounter
{
private readonly GCounter _increments;
private readonly GCounter _decrements;
public PNCounter(string nodeId)
{
_increments = new GCounter(nodeId + ":inc");
_decrements = new GCounter(nodeId + ":dec");
}
public void Increment() => _increments.Increment();
public void Decrement() => _decrements.Increment();
public long Value => _increments.Value - _decrements.Value;
public void Merge(PNCounter other)
{
_increments.Merge(other._increments);
_decrements.Merge(other._decrements);
}
}
Common CRDT Data Types
| CRDT Type | Operation | Merge Strategy | Use Case |
|---|---|---|---|
| G-Counter | Increment only | Max of each node's counter | Page view counts, like counts |
| PN-Counter | Increment and decrement | Separate G-Counters for +/- | Inventory levels, balance tracking |
| LWW-Register | Set value | Keep value with latest timestamp | User profile fields, config values |
| OR-Set | Add/remove elements | Observed-Remove semantics | Shopping carts, tag sets |
| LVN (Last-Value Register) | Set value with version | Keep value with highest version | User preferences |
| Leaderboard CRDT | Update scores | Keep top N by score | Gaming leaderboards |
C#
// OR-Set (Observed-Remove Set) — supports add and remove
public class ORSet<T>
{
private readonly Dictionary<T, HashSet<string>> _elements = new();
private readonly string _nodeId;
private int _tagCounter = 0;
public ORSet(string nodeId) => _nodeId = nodeId;
public void Add(T element)
{
var tag = $"{_nodeId}:{_tagCounter++}";
if (!_elements.ContainsKey(element))
_elements[element] = new HashSet<string>();
_elements[element].Add(tag);
}
public bool Remove(T element)
{
// Observed-remove: remove all tags we've seen for this element
if (_elements.TryGetValue(element, out var tags))
{
_elements.Remove(element);
return true;
}
return false;
}
public void Merge(ORSet<T> other)
{
foreach (var (element, otherTags) in other._elements)
{
if (!_elements.ContainsKey(element))
_elements[element] = new HashSet<string>();
// Union of tags: keeps elements that either side has added
// and not explicitly removed by both sides
_elements[element].UnionWith(otherTags);
}
}
public bool Contains(T element) =>
_elements.ContainsKey(element) && _elements[element].Count > 0;
public IReadOnlyCollection<T> Elements =>
_elements.Where(kv => kv.Value.Count > 0)
.Select(kv => kv.Key)
.ToList();
}
12. Quorum-Based Read and Write Systems
Quorum-based systems provide a tunable middle ground between strong consistency and high availability. By adjusting the number of nodes that must acknowledge reads and writes, you can trade latency for consistency. A quorum is defined as a majority: for N replicas, a quorum is (N/2) + 1 nodes. If every write is acknowledged by a quorum and every read is served by a quorum, the read and write quorums must overlap, guaranteeing that every read sees the latest write. This is the mathematical foundation of tunable consistency.
The Quorum Equation
For strong consistency: W + R > N, where W is the write quorum, R is the read quorum, and N is the total number of replicas. This ensures that at least one node in the read quorum has the latest write. If W = N (all nodes must acknowledge), R = 1 is sufficient. If W = 1 (any single node acknowledges), R = N is required. The most common configuration is W = majority and R = majority, providing strong consistency with reasonable latency.
C#
// Quorum-based distributed store
public class QuorumStore<T>
{
private readonly IReadOnlyList<IReplica<T>> _replicas;
private readonly int _writeQuorum;
private readonly int _readQuorum;
public QuorumStore(IReadOnlyList<IReplica<T> replicas, int replicationFactor)
{
_replicas = replicas;
// Default quorum: majority
_writeQuorum = (replicationFactor / 2) + 1;
_readQuorum = (replicationFactor / 2) + 1;
}
public QuorumStore(
IReadOnlyList<IReplica<T>> replicas,
int writeQuorum, int readQuorum)
{
_replicas = replicas;
_writeQuorum = writeQuorum;
_readQuorum = readQuorum;
}
public async Task<bool> WriteAsync(string key, T value)
{
var tasks = _replicas.Select(r => r.WriteAsync(key, value));
var results = await Task.WhenAll(tasks);
var successCount = results.Count(r => r);
if (successCount >= _writeQuorum)
return true; // Quorum achieved
throw new QuorumException(
$"Write quorum not met: {successCount}/{_writeQuorum}");
}
public async Task<T> ReadAsync(string key)
{
var tasks = _replicas.Select(r => r.ReadAsync(key));
var results = await Task.WhenAll(tasks);
var validResults = results.Where(r => r != null).ToList();
if (validResults.Count < _readQuorum)
throw new QuorumException(
$"Read quorum not met: {validResults.Count}/{_readQuorum}");
// Return the value with the latest version/timestamp
return validResults
.OrderByDescending(r => r.Version)
.First().Value;
}
}
// Cassandra-style tunable consistency per query
public class TunableConsistencyClient
{
private readonly QuorumStore<string> _store;
public async Task<string> ReadStrongAsync(string key)
{
// QUORUM read: W + R > N guarantees seeing latest write
return await _store.ReadAsync(key); // R = majority
}
public async Task<string> ReadFastAsync(string key)
{
// ONE read: fastest, but may be stale
var result = await _store.ReadFromReplicaAsync(key, replicaIndex: 0);
return result;
}
}
Consistency Level Configurations
| Configuration | Write Quorum | Read Quorum | Consistency | Availability | Latency |
|---|---|---|---|---|---|
| ONE / ONE | 1 | 1 | Eventual | Highest | Lowest |
| QUORUM / QUORUM | (N/2)+1 | (N/2)+1 | Strong | Moderate | Moderate |
| ALL / ONE | N | 1 | Strong | Lowest (any node failure blocks writes) | Write: high, Read: low |
| ONE / ALL | 1 | N | Strong | Lowest (any node failure blocks reads) | Write: low, Read: high |
| LOCAL_QUORUM | Local DC majority | Local DC majority | Strong within DC | Cross-DC failure tolerant | Low (local) |
| EACH_QUORUM | Majority in each DC | Majority in each DC | Global strong | Lower (all DCs must be up) | Higher (cross-DC) |
Hinted Handoff
When a write cannot reach a replica (because that node is down), the system stores a "hint" — a temporary record of the write — on another healthy node. When the downed node recovers, the hint is replayed, ensuring the write eventually reaches all replicas. This is a key availability optimization in AP systems: writes can succeed even when some replicas are unavailable, with the guarantee that they will be delivered eventually.
C#
// Hinted handoff for AP system availability
public class HintedHandoffStore<T>
{
private readonly IReadOnlyList<IReplica<T>> _replicas;
private readonly IHintStore _hints;
public async Task WriteAsync(string key, T value)
{
var writeTasks = _replicas.Select(async replica =>
{
try
{
await replica.WriteAsync(key, value);
}
catch (NodeUnavailableException)
{
// Store a hint on a healthy node
var healthy = _replicas.First(r =>
r.Id != replica.Id && r.IsHealthy);
await _hints.StoreAsync(new Hint
{
TargetNodeId = replica.Id,
Key = key,
Value = value,
Timestamp = DateTime.UtcNow
});
}
});
await Task.WhenAll(writeTasks);
}
public async Task ReplayHintsAsync(string recoveredNodeId)
{
var hints = await _hints.GetForNodeAsync(recoveredNodeId);
foreach (var hint in hints.OrderBy(h => h.Timestamp))
{
var target = _replicas.First(r => r.Id == recoveredNodeId);
await target.WriteAsync(hint.Key, hint.Value);
await _hints.DeleteAsync(hint.Id);
}
}
}
13. Real-World Case Studies
Understanding how major companies navigate the CAP trade-offs provides practical insights that no textbook can offer. Each case study reveals a different approach to balancing consistency, availability, and performance in production systems serving millions of users.
Google Spanner: The CP Dream
Google Spanner is the most ambitious CP system in production. It provides globally consistent transactions across data centers using TrueTime — a combination of atomic clocks and GPS receivers that provides clock uncertainty bounded to under 7ms. By waiting out the uncertainty window before committing transactions, Spanner achieves external consistency (linearizability) across geographically distributed nodes. The cost is latency: every write must wait for the TrueTime uncertainty to resolve, adding 7-10ms of artificial delay. But for financial transactions and globally consistent data, this is an acceptable trade-off.
Spanner's architecture directly addresses the CAP trade-off. During a network partition, nodes in the minority partition cannot commit new transactions because they cannot obtain a TrueTime timestamp that is guaranteed to be globally ordered. This means some clients experience unavailability during partitions — the CP choice. However, reads using stale read timestamps (a feature called "stale reads") can be served from any replica without coordination, providing AP-like read availability for non-critical queries.
C#
// Spanner-style TrueTime simulation
public class TrueTimeSimulator
{
private readonly IClockSource _atomicClock;
private readonly IClockSource _gpsReceiver;
private readonly TimeSpan _uncertainty;
public TrueTimeSimulator()
{
_atomicClock = new AtomicClockSource();
_gpsReceiver = new GpsClockSource();
// TrueTime uncertainty is typically 1-7ms
_uncertainty = TimeSpan.FromMilliseconds(4);
}
public TrueTimeInterval Now()
{
var atomic = _atomicClock.Now();
var gps = _gpsReceiver.Now();
var earliest = atomic - _uncertainty;
var latest = atomic + _uncertainty;
return new TrueTimeInterval(earliest, latest);
}
// Spanner waits until the uncertainty window passes before committing
public async Task<DateTime> CommitTimestampAsync()
{
var interval = Now();
var now = DateTime.UtcNow;
var waitTime = interval.Latest - now;
if (waitTime > TimeSpan.Zero)
await Task.Delay(waitTime);
return DateTime.UtcNow; // After waiting, this is in the "certain" future
}
}
Amazon DynamoDB: The AP Powerhouse
Amazon DynamoDB, inspired by the original Dynamo paper, is a PA/EL system optimized for low-latency reads and writes. During normal operation, DynamoDB replicates data across three AZs with synchronous replication within a region and asynchronous replication across regions. Writes use a quorum protocol (W=2 of 3 replicas in a single region), providing strong consistency within a region. During normal operation, DynamoDB achieves single-digit millisecond latency for both reads and writes — the EL (low latency) choice in PACELC.
DynamoDB's multi-master global tables take the AP path: writes are accepted in any region and replicated asynchronously to other regions. Conflict resolution uses last-write-wins based on DynamoDB's physical timestamps. This means writes to the same item in different regions may be lost (the later write wins). For applications that need global consistency, DynamoDB offers global tables with strong consistency (but higher latency and lower availability during cross-region partitions).
Cassandra: Tunable PA/EC
Apache Cassandra is the canonical example of a tunable PA/EC system. By default, it uses ONE for reads and writes (PA/EL — available and low latency). But with QUORUM, it achieves strong consistency (PA/EC — available during partition, but consistent during normal operation). Cassandra's genius is putting this choice in the hands of the application developer: use ONE for non-critical data (user preferences, analytics), QUORUM for critical data (shopping cart, inventory), and ALL for ultra-critical data (financial balances).
| Company | System | PACELC | Key Innovation | Scale |
|---|---|---|---|---|
| Spanner | PC/EC | TrueTime for global consistency | Millions of nodes globally | |
| Amazon | DynamoDB | PA/EL | Multi-AZ with single-digit ms latency | Hundreds of millions of requests/day |
| Apache | Cassandra | PA/EC (tunable) | Per-query consistency levels | Petabytes of data |
| Meta | TAO (Graph) | PA/EL | Eventually consistent graph for social graph | Billions of objects |
| Netflix | EVCache | PA/EL | Eventually consistent caching layer | Hundreds of millions of reads/day |
| Microsoft | Cosmos DB | Tunable (all options) | Five consistency levels, multi-master | Billions of requests/day globally |
14. Designing for CAP Trade-offs in Practice
Translating CAP theory into architectural decisions requires systematic analysis of your application's data flows. This section provides a practical framework for making these decisions, along with C# implementations of common patterns.
Step 1: Classify Your Data Paths
Every data path in your system has different consistency requirements. Classify each path as Critical (must be strongly consistent), Important (should be consistent with bounded staleness), or Best-Effort (eventual consistency is acceptable). This classification drives your technology and consistency-level choices for each component.
C#
// Consistency requirement classification
public enum ConsistencyRequirement
{
Strong, // Linearizable: payment, inventory, authentication
Bounded, // Bounded staleness: read-your-writes, session data
Eventual, // Eventual: analytics, feeds, non-critical reads
None // No consistency needed: metrics, logs
}
public class DataPathClassifier
{
private readonly Dictionary<string, ConsistencyRequirement> _classifications = new()
{
["payment.process"] = ConsistencyRequirement.Strong,
["inventory.reserve"] = ConsistencyRequirement.Strong,
["user.profile.read"] = ConsistencyRequirement.Bounded,
["user.profile.write"] = ConsistencyRequirement.Bounded,
["analytics.event"] = ConsistencyRequirement.Eventual,
["feed.timeline"] = ConsistencyRequirement.Eventual,
["metrics.emit"] = ConsistencyRequirement.None
};
public ConsistencyRequirement GetRequirement(string dataPath) =>
_classifications.GetValueOrDefault(dataPath, ConsistencyRequirement.Eventual);
}
Step 2: Choose the Right Technology
Match each data path's consistency requirement to the appropriate storage technology. Strong consistency requires a CP system (etcd, Spanner, CockroachDB). Bounded staleness can use read-your-writes sessions on a CP system or a tunable system at QUORUM level. Eventual consistency can use any AP system (Cassandra, DynamoDB, Redis).
Step 3: Design for Failure
CAP is only relevant during failures. Design your system to detect failures quickly, degrade gracefully, and recover automatically. Use circuit breakers to prevent cascading failures, fallbacks to serve degraded but available responses, and monitoring to detect consistency violations before they impact users.
C#
// Graceful degradation based on consistency requirement
public class ConsistencyAwareClient
{
private readonly ICpStore _cpStore; // etcd, Spanner
private readonly IApStore _apStore; // Cassandra, DynamoDB
private readonly ICacheStore _cache; // Redis
public async Task<T> ReadAsync<T>(
string key, ConsistencyRequirement requirement)
{
return requirement switch
{
ConsistencyRequirement.Strong =>
await _cpStore.ReadAsync(key), // Always from CP source
ConsistencyRequirement.Bounded =>
await ReadWithBoundedStalenessAsync(key),
ConsistencyRequirement.Eventual =>
await ReadWithEventualConsistencyAsync(key),
ConsistencyRequirement.None =>
await _cache.GetAsync<T>(key), // Fastest, may be stale
};
}
private async Task<T> ReadWithBoundedStalenessAsync<T>(string key)
{
// Try cache first (may be stale but fast)
var cached = await _cache.GetAsync<T>(key);
if (cached != null) return cached;
// Fall back to CP store for consistency
return await _cpStore.ReadAsync(key);
}
private async Task<T> ReadWithEventualConsistencyAsync<T>(string key)
{
// Read from the nearest replica (AP store, low latency)
return await _apStore.ReadAsync(key);
}
}
Step 4: Handle Conflicts Correctly
For AP systems, design conflict resolution into your data model. Use CRDTs for data types that can be merged automatically (counters, sets). Use application-level merge for domain-specific data (shopping carts, documents). Use conflict logs for data that requires human review. Never use last-write-wins for data where correctness matters — it silently loses writes.
Some teams respond to CAP complexity by forcing strong consistency on all data paths. This "solves" the consistency problem but introduces severe availability and performance issues. Strong consistency requires majority quorums, adding latency to every operation. During any node failure, writes may be rejected. At geographic scale, cross-continent round trips add hundreds of milliseconds. The correct approach is per-path consistency, not global consistency.
15. Tunable Consistency: The Modern Approach
Modern distributed databases have moved beyond the binary CP/AP choice by offering tunable consistency. This allows developers to choose the appropriate consistency level for each operation, balancing correctness, performance, and availability on a per-query basis. This is the most practical application of CAP theory in modern system design.
Cosmos DB: Five Consistency Levels
Microsoft Cosmos DB offers five consistency levels, from strongest to weakest: Strong (linearizable), Bounded Staleness (stale by at most K versions or T seconds), Session (read-your-writes within a session), Consistent Prefix (reads never see out-of-order writes), and Eventual (no ordering guarantee). Each level has different latency, throughput, and availability characteristics, and you can change the level without reindexing or migrating data.
C#
// Cosmos DB: Tunable consistency per container
public class CosmosDbTunableClient
{
private readonly CosmosClient _client;
// Strong consistency for financial transactions
public async Task<Transaction> GetTransactionAsync(string id)
{
var container = _client.GetContainer("finance", "transactions");
var response = await container.ReadItemAsync<Transaction>(
id, new PartitionKey(id));
return response.Resource;
}
// Session consistency for user profiles (read-your-writes)
public async Task<UserProfile> GetProfileAsync(
string userId, string sessionId)
{
var options = new ItemRequestOptions
{
ConsistencyLevel = ConsistencyLevel.Session,
SessionToken = sessionId
};
var container = _client.GetContainer("social", "profiles");
var response = await container.ReadItemAsync<UserProfile>(
userId, new PartitionKey(userId), options);
return response.Resource;
}
// Eventual consistency for analytics (lowest latency)
public async Task<AnalyticsEvent> GetLatestEventAsync(string entityId)
{
var options = new ItemRequestOptions
{
ConsistencyLevel = ConsistencyLevel.Eventual
};
var container = _client.GetContainer("analytics", "events");
var response = await container.ReadItemAsync<AnalyticsEvent>(
entityId, new PartitionKey(entityId), options);
return response.Resource;
}
}
Cassandra: Per-Query Tunable
Cassandra allows setting the consistency level per query. This means the same table can be read at ONE consistency for a dashboard feed and at QUORUM consistency for a payment verification query. The flexibility is powerful but requires discipline — developers must understand when to use which level to avoid subtle bugs (like reading stale data for a critical operation).
CockroachDB: Follower Reads
CockroachDB offers "follower reads" — reads from a follower replica that may be slightly stale (up to the lease interval, typically 4.8 seconds). These reads bypass the leader, reducing latency significantly. The staleness is bounded and predictable, making it suitable for read-heavy workloads where slightly stale data is acceptable (analytics dashboards, read replicas for reporting).
| Database | Strongest Level | Weakest Level | Granularity | Latency Difference |
|---|---|---|---|---|
| Cosmos DB | Strong (linearizable) | Eventual | Per-account or per-query | 2-10x |
| Cassandra | ALL | ONE | Per-query | 2-5x |
| CockroachDB | Serializable (leader read) | Follower read (bounded staleness) | Per-query | 2-3x |
| DynamoDB | Strongly Consistent Read | Eventually Consistent Read | Per-read-operation | 1.5-3x |
| MongoDB | Linearizable (majority) | Eventual (secondaryPreferred) | Per-query or per-connection | 2-5x |
16. Monitoring Distributed Systems for CAP Violations
Distributed systems fail silently. A consistency violation might mean a user sees stale data, a financial transaction processes twice, or an inventory counter drifts. Monitoring for CAP-related issues requires specific metrics that detect inconsistency, unavailability, and partition events before they impact users.
Key Metrics
| Metric | What It Detects | Alert Threshold |
|---|---|---|
| Replication lag | Stale reads on followers | > 1 second for strong consistency reads |
| Quorum failure rate | Inability to achieve write/read quorum | > 0.1% of operations |
| Conflict rate | AP system write conflicts | > 1% of writes (indicates partition or clock skew) |
| Leader election frequency | CP system instability | > 2 elections per hour |
| Read-your-writes violation rate | Session consistency broken | > 0.01% of reads |
| Anti-entropy repair rate | Replicas drifting apart | > 10% of reads triggering repair |
C#
// Consistency monitoring service
public class ConsistencyMonitor
{
private readonly IMetricsCollector _metrics;
private readonly IReplicaHealthChecker _health;
private readonly IAlertingService _alerts;
public async Task CheckConsistencyHealthAsync()
{
// Check replication lag across all replicas
var lagReport = await _health.GetReplicationLagAsync();
foreach (var replica in lagReport.Replicas)
{
_metrics.Gauge("replication_lag_ms", replica.LagMs,
new { replica = replica.Id, database = replica.Database });
if (replica.LagMs > 1000) // > 1 second
{
await _alerts.AlertAsync(AlertSeverity.Warning,
$"Replication lag {replica.Id}: {replica.LagMs}ms — " +
$"strong consistency reads may see stale data");
}
}
// Check quorum availability
var quorumStatus = await _health.GetQuorumStatusAsync();
foreach (var cluster in quorumStatus.Clusters)
{
_metrics.Gauge("quorum_available", cluster.HasQuorum ? 1 : 0,
new { cluster = cluster.Name });
if (!cluster.HasQuorum)
{
await _alerts.AlertAsync(AlertSeverity.Critical,
$"Quorum lost for cluster {cluster.Name} — " +
$"CP operations will be rejected");
}
}
// Check for consistency violations (read-after-write failures)
var violationRate = await _metrics.GetGaugeAsync(
"consistency_violation_rate");
if (violationRate > 0.0001) // > 0.01%
{
await _alerts.AlertAsync(AlertSeverity.Critical,
$"Consistency violation rate: {violationRate:P4} — " +
$"read-after-write violations detected");
}
}
}
Distributed Tracing for CAP Debugging
When consistency issues arise, distributed tracing is essential for debugging. Every read and write should be traced with: the consistency level used, which replicas were contacted, the quorum result, the replication lag at the time of the operation, and whether any fallback was used. This trace data enables post-incident analysis to determine whether a consistency violation was caused by clock skew, replication lag, partition, or application bug.
C#
// Distributed tracing for CAP-aware operations
public class TracedDistributedStore
{
private readonly IDistributedStore _store;
private readonly ITracer _tracer;
public async Task<T> ReadAsync<T>(string key, ConsistencyLevel level)
{
using var span = _tracer.StartSpan("distributed.read");
span.SetTag("consistency_level", level.ToString());
span.SetTag("key", key);
try
{
var result = await _store.ReadAsync<T>(key, level);
span.SetTag("result.success", true);
span.SetTag("result.replicas_contacted",
_store.LastReplicasContacted);
span.SetTag("result.quorum_achieved",
_store.LastQuorumAchieved);
return result;
}
catch (QuorumException ex)
{
span.SetTag("result.success", false);
span.SetTag("result.error", "quorum_not_met");
span.SetTag("replicas_available",
_store.AvReplicasAvailable);
span.SetTag("replicas_total", _store.TotalReplicas);
throw;
}
}
}
17. Codebase Structure for CAP-Aware Systems
Building a CAP-aware distributed system requires a clean architecture that separates consistency decisions from business logic. The codebase should make it easy to change the consistency level for any operation without modifying the business logic. This separation enables testing different consistency configurations and migrating between consistency levels as requirements evolve.
Architecture Pattern
Project Structure
CapAwareDistributedSystem/
├── src/
│ ├── Domain/ # Business logic, no infrastructure deps
│ │ ├── Models/
│ │ │ ├── Order.cs
│ │ │ ├── Payment.cs
│ │ │ └── Inventory.cs
│ │ ├── Services/
│ │ │ ├── OrderService.cs # Business rules only
│ │ │ └── PaymentService.cs
│ │ └── Interfaces/
│ │ ├── IOrderRepository.cs # Abstract storage interface
│ │ └── IPaymentRepository.cs
│ │
│ ├── Infrastructure/ # Storage implementations
│ │ ├── StrongConsistency/
│ │ │ ├── CockroachDbRepository.cs # CP: strong consistency
│ │ │ └── EtcdLockProvider.cs # CP: distributed locks
│ │ ├── TunableConsistency/
│ │ │ ├── CassandraRepository.cs # Tunable per query
│ │ │ └── CosmosDbRepository.cs # 5 consistency levels
│ │ ├── EventualConsistency/
│ │ │ ├── DynamoDbRepository.cs # AP: eventual consistency
│ │ │ └── RedisCacheRepository.cs # AP: cache layer
│ │ └── ConflictResolution/
│ │ ├── LastWriteWinsResolver.cs
│ │ ├── VectorClockResolver.cs
│ │ └── CrdtMergeEngine.cs
│ │
│ ├── Api/ # HTTP endpoints
│ │ ├── Controllers/
│ │ │ ├── OrdersController.cs
│ │ │ └── PaymentsController.cs
│ │ └── Middleware/
│ │ ├── ConsistencyLevelMiddleware.cs
│ │ └── TracingMiddleware.cs
│ │
│ └── Monitoring/
│ ├── ConsistencyHealthCheck.cs
│ └── ReplicaLagMonitor.cs
│
├── tests/
│ ├── UnitTests/ # Business logic tests
│ ├── ConsistencyTests/ # Verify consistency guarantees
│ │ ├── StrongConsistencyTests.cs
│ │ ├── EventualConsistencyTests.cs
│ │ └── ConflictResolutionTests.cs
│ └── PartitionTests/ # Chaos testing for CAP behavior
│ ├── PartitionDetectionTests.cs
│ └── QuorumLossTests.cs
C#
// Dependency injection with consistency level selection
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCapAwareStorage(
this IServiceCollection services,
ConsistencyConfig config)
{
// Register storage implementations based on configuration
switch (config.DefaultConsistency)
{
case ConsistencyLevel.Strong:
services.AddSingleton<IOrderRepository, CockroachDbRepository>();
break;
case ConsistencyLevel.Session:
case ConsistencyLevel.BoundedStaleness:
services.AddSingleton<IOrderRepository, CassandraRepository>();
break;
case ConsistencyLevel.Eventual:
services.AddSingleton<IOrderRepository, DynamoDbRepository>();
break;
}
// Always register conflict resolution for AP stores
services.AddSingleton<IConflictResolver, SmartConflictResolver>();
// Register consistency-aware wrapper
services.Decorate<IOrderRepository>((inner, provider) =>
new ConsistencyAwareRepository(inner,
provider.GetRequiredService<IConsistencyConfig>()));
return services;
}
}
// Consistency-aware repository decorator
public class ConsistencyAwareRepository : IOrderRepository
{
private readonly IOrderRepository _inner;
private readonly IConsistencyConfig _config;
public ConsistencyAwareRepository(
IOrderRepository inner, IConsistencyConfig config)
{
_inner = inner;
_config = config;
}
public async Task<Order> GetAsync(Guid id, ConsistencyLevel? level = null)
{
var effectiveLevel = level ?? _config.GetLevel("order.read");
return effectiveLevel switch
{
ConsistencyLevel.Strong =>
await _inner.GetAsync(id), // Leader/strong read
ConsistencyLevel.Session =>
await _inner.GetFromSessionAsync(id), // Read-your-writes
ConsistencyLevel.Eventual =>
await _inner.GetFromNearestReplicaAsync(id), // Fastest
_ => await _inner.GetAsync(id)
};
}
}
18. Interview Q&A Deep Dive
The following questions and answers cover the most commonly asked CAP theorem and distributed systems questions in senior+ system design interviews. Each answer provides the theoretical foundation plus practical engineering details that demonstrate depth of understanding.
Q1: Can you explain the CAP theorem and why it matters for system design?
Answer: The CAP theorem states that a distributed data store can provide at most two of three guarantees: Consistency (every read receives the most recent write), Availability (every request receives a non-error response), and Partition Tolerance (the system operates despite network partitions). Since network partitions are unavoidable in distributed systems, the real choice is between consistency and availability during partitions. CP systems (etcd, Spanner) reject requests from minority partitions to maintain consistency. AP systems (Cassandra, DynamoDB) continue serving requests but may return stale data. The theorem matters because every distributed system architect faces this trade-off — acknowledging it explicitly leads to better design decisions than ignoring it.
Q2: Is the CAP theorem still relevant? What about the PACELC theorem?
Answer: CAP is relevant but often oversimplified. It only describes behavior during partitions, but systems make consistency-latency trade-offs during normal operation too. The PACELC theorem extends CAP: during normal operation (no partition), you still choose between latency and consistency. DynamoDB is PA/EL (available during partition, low latency normally). Spanner is PC/EC (consistent during partition, consistent normally — with higher latency). Cassandra is tunable: PA/EL at ONE consistency, PA/EC at QUORUM. PACELC is more practically useful than CAP because it addresses the 99.99% of the time when there is no partition.
Q3: When should you use strong consistency vs eventual consistency?
Answer: Use strong consistency for operations where stale data causes real harm: financial transactions (double-spending), inventory management (overselling), authentication (credential verification), and ordering systems (duplicate orders). Use eventual consistency for operations where temporary staleness is acceptable: social media feeds (showing a post from 2 seconds ago is fine), analytics dashboards (slightly stale metrics are acceptable), content delivery (CDN cache lag), and user profile views (seeing an old avatar briefly is not a disaster). The key insight is to use the weakest consistency that works for each data path — this maximizes availability and performance while maintaining correctness.
Q4: Explain quorum-based consistency with an example.
Answer: In a system with N replicas, a quorum is (N/2) + 1 nodes. For strong consistency, W + R > N, where W is the write quorum and R is the read quorum. With N=5, W=3, R=3: a write to 3 replicas and a read from 3 replicas must overlap by at least 1 node (3+3-5=1), guaranteeing the read sees the latest write. If we use W=1, R=5, we get the same consistency but with different latency characteristics (fast writes, slow reads). Cassandra uses this model: ONE (fast, eventual), QUORUM (balanced, strong), ALL (slowest, strongest). The choice per query gives fine-grained control over the consistency-performance trade-off.
Q5: How do CRDTs solve the consistency problem?
Answer: CRDTs (Conflict-Free Replicated Data Types) are data structures that can be merged automatically without coordination. They guarantee strong eventual consistency: if all replicas receive the same updates (in any order), they converge to the same state. For example, a G-Counter (grow-only counter) maintains per-node counts. Merging two replicas takes the max of each node's count — this operation is commutative, associative, and idempotent. The limitation is that CRDTs only work for specific data types (counters, sets, registers, maps) and cannot express arbitrary business logic. They are ideal for distributed counters, shopping carts, collaborative editing, and leaderboards.
Q6: What is the difference between linearizability and sequential consistency?
Answer: Linearizability requires that every operation appears to take effect atomically at some point between its invocation and response, consistent with real-time ordering. Sequential consistency requires that all operations appear in some total order consistent with each process's program order, but the order may not correspond to real-time. Example: Process 1 writes x=1 then x=2. Process 2 reads x and sees 1 then 2. This is sequentially consistent. But if Process 2 reads 2 then 1, it violates sequential consistency. Linearizability additionally requires that if Process 1's write of x=2 completes before Process 2 reads x, Process 2 must see 2. Sequential consistency allows seeing 1 in that case (if the read started before the write completed in real time). Linearizability is stronger but costs more latency because it requires coordination with a majority on every operation.
Q7: How do you design a system that degrades gracefully during partitions?
Answer: Design a tiered consistency architecture. Identify which data paths are critical (must be consistent), important (bounded staleness acceptable), and best-effort (eventual is fine). During normal operation, use the appropriate consistency level for each tier. During a partition, degrade gracefully: critical paths reject requests (CP choice) to prevent data corruption, important paths serve from the nearest replica with bounded staleness (read-your-writes if possible), best-effort paths continue serving from any available replica. The implementation uses a circuit breaker per data path — when a CP path loses quorum, it opens the circuit and returns a "temporarily unavailable" response, while AP paths continue normally. This provides maximum availability while protecting data correctness.
Q8: Explain how Raft achieves consensus and why it is preferred over Paxos.
Answer: Raft decomposes consensus into leader election, log replication, and safety. A leader is elected via a majority vote. The leader accepts all writes and replicates log entries to followers. A write is committed when a majority of nodes acknowledge it. If the leader fails, followers detect the election timeout and elect a new leader. Raft provides the same safety guarantees as Multi-Paxos but is dramatically easier to implement and reason about because it uses a strong leader model (all writes go through one leader) and explicit log structure. Most production systems use Raft: etcd (Kubernetes backing store), Consul (service discovery), CockroachDB (distributed SQL), and TiKV (TiDB storage engine). Paxos is used by Google Spanner, but even Google acknowledges its implementation complexity.
Q9: How do you handle the split-brain problem?
Answer: Split-brain occurs when a network partition causes two groups of nodes to both believe they are the primary, accepting conflicting writes. Solutions: (1) Fencing tokens — each leader gets a monotonically increasing token. If a stale leader tries to write, the storage layer rejects it because the token is older. (2) Majority quorum — only the partition with a majority of nodes can elect a leader and accept writes. The minority partition must stop serving. (3) Lease-based leadership — the leader holds a time-limited lease. During a partition, the stale leader's lease expires while the new leader acquires a fresh lease. (4) External coordination — use ZooKeeper or etcd as a source of truth for leadership, ensuring only one leader exists globally. Google Spanner uses TrueTime to prevent split-brain by ensuring all nodes agree on the current time within a bounded uncertainty.
Q10: What are the practical limits of CAP-aware design?
Answer: The practical limits are: (1) CAP only models a single data item — real systems must reason about consistency across multiple items (distributed transactions, multi-key operations). (2) CAP assumes a binary partition — real partitions are often partial or asymmetric, making the C/A choice ambiguous. (3) CAP does not model latency — PACELC addresses this by considering the latency-consistency trade-off during normal operation. (4) CAP assumes an asynchronous network model — real networks have bounded delays, which some protocols (like Spanner's TrueTime) exploit to provide consistency with bounded unavailability. (5) CAP does not address Byzantine failures — it assumes nodes follow the protocol but may crash. For systems requiring Byzantine fault tolerance, you need protocols like PBFT (Practical Byzantine Fault Tolerance). Understanding these limits prevents over-relying on CAP as a design framework and encourages thinking about the full picture of distributed systems constraints.
Key Numbers to Remember
| Concept | Key Value |
|---|---|
| Quorum formula | W + R > N for strong consistency |
| Raft election timeout | 150-300ms randomized |
| Raft majority | (N/2) + 1 nodes for any decision |
| TrueTime uncertainty | Under 7ms (Google data centers) |
| Typical replication lag | 1-10ms within a datacenter, 50-200ms cross-region |
| Cassandra ONE vs QUORUM latency | ONE: ~2ms, QUORUM: ~10ms (typical) |
| Paxos minimum rounds | 2 rounds (prepare + accept) |
| 2PC blocking time | Indefinite until coordinator recovers |
| Saga compensation window | Until the next step completes |
| Eventual consistency convergence | Depends on anti-entropy interval (typically seconds to minutes) |
Pre-Interview Checklist
- Define CAP formally (linearizability, not just "consistency")
- Explain why partition tolerance is non-negotiable
- Classify 5+ real databases as CP or AP with justification
- Explain PACELC and why it extends CAP
- Describe Raft consensus (leader election, log replication, commit)
- Compare 2PC with Sagas (blocking vs non-blocking, atomic vs eventual)
- Explain quorum math (W + R > N) with examples
- Describe CRDTs and when to use them
- Discuss tunable consistency (Cosmos DB, Cassandra, CockroachDB)
- Know how to monitor for consistency violations (replication lag, quorum failures)
- Explain split-brain and fencing tokens
- Design a tiered consistency architecture for a specific use case