Strong vs. Eventual Consistency
How up-to-date must a read be relative to the last successful write? Linearizability vs. causal vs. eventual consistency models.
Intent & Description
🎯 Intent
Choose the right consistency model for your use case based on how critical data freshness is versus latency and availability requirements.
📋 Context
Different applications require different consistency guarantees. Financial systems need linearizability (every read reflects the latest write globally). Social feeds can tolerate eventual consistency (replicas converge over time). The spectrum includes linearizability, sequential consistency, causal consistency, read-your-writes, monotonic reads, and eventual consistency.
💡 Solution
Use read-your-writes consistency at session level for most user-facing apps — achievable by routing reads to the same replica. Use linearizability only for coordination primitives (distributed locks, leader election). Use CRDTs for eventual consistency without manual conflict resolution. Implement monotonic reads for pagination and feeds.
Real-world Use Case
📌 TL;DR
Consistency spectrum: linearizability (strongest, most expensive) → causal → read-your-writes → monotonic → eventual (weakest, cheapest). Choose based on freshness requirements vs. latency cost.
Advantages
- Clear consistency models map to business requirements
- Read-your-writes is cheap and sufficient for most user apps
- CRDTs enable automatic conflict resolution
- Spectrum approach allows per-operation consistency tuning
Disadvantages
- Linearizability is expensive (requires quorum, high latency)
- Eventual consistency can confuse users if not handled well
- CRDTs add implementation complexity
- Consistency model changes are difficult after deployment
// Consistency Levels in Practice
class ConsistentDatabase {
async read(key, consistency = 'eventual') {
switch (consistency) {
case 'linearizable':
// Wait for quorum, ensure latest write
return await this.readFromQuorum(key);
case 'causal':
// Ensure causally related ops are seen in order
return await this.readWithCausalTracking(key);
case 'read_your_writes':
// Route to same replica user wrote to
const replica = this.getUserReplica(key);
return await replica.read(key);
case 'monotonic':
// Never go backwards in time
return await this.readWithVersionCheck(key);
case 'eventual':
// Read from any replica, fastest
return await this.readFromAnyReplica(key);
}
}
}
// CRDT Example: Conflict-free replicated counter
class GCounter {
constructor() {
this.counts = new Map(); // node_id -> count
}
increment(nodeId) {
this.counts.set(nodeId, (this.counts.get(nodeId) || 0) + 1);
}
merge(otherCounter) {
for (const [nodeId, count] of otherCounter.counts) {
this.counts.set(nodeId, Math.max(this.counts.get(nodeId) || 0, count));
}
}
value() {
return Array.from(this.counts.values()).reduce((a, b) => a + b, 0);
}
}