Consistency Models — Strong, Eventual, Causal Consistency and CRDTs Explained
A consistency model is a contract between a distributed data store and its clients that defines the allowed ordering of read and write operations, determining how quickly updates propagate across nodes.
Why Consistency Models Matter
Every distributed system must balance freshness against performance and availability. Banks need strong consistency — you can’t see a balance of $100 and withdraw $200 because a node hasn’t yet seen your deposit. Social media apps tolerate eventual consistency — it’s acceptable if a like takes a few seconds to appear globally. The choice of consistency model directly impacts user experience, system complexity, and operational cost. Distributed Systems like Amazon DynamoDB chose eventual consistency to achieve high availability at global scale.
Plain-Language Explanation
Imagine a group chat with three friends: Alice, Bob, and Charlie. When Alice sends “Let’s meet at 6pm,” Bob sees it immediately on his phone. Charlie was on a plane and receives it three hours later. During those three hours, Bob and Charlie have different views of the conversation. When Charlie finally lands, his phone catches up.
- Strong consistency would mean Charlie can’t message anyone until he sees Alice’s message. The conversation pauses until everyone is synchronized.
- Eventual consistency means Charlie eventually sees everything, but there’s a period where his view is stale.
- Causal consistency would ensure that if Bob replies “Sounds good” after Alice’s message, Charlie sees “Sounds good” only after seeing Alice’s original message — maintaining cause and effect.
graph LR
subgraph "Consistency Spectrum"
Strong[Strong
High latency
Strict ordering]
Eventual[Eventual
Low latency
Convergence only]
Causal[Causal
Causality preserved
Concurrent unordered]
end
Strong --> Causal
Causal --> Eventual
UseCase1[Banking, Payments] --> Strong
UseCase2[Social Media, DNS] --> Eventual
UseCase3[Collaborative Editing] --> Causal
style Strong fill:#e74c3c,color:#fff
style Eventual fill:#27ae60,color:#fff
style Causal fill:#f39c12,color:#fff
Strong Consistency
Every read returns the most recent write. All nodes agree on the order of operations. Like a single-node database.
Implementation: Requires consensus (Paxos, Raft) or synchronous replication. A write is acknowledged only after a majority of nodes confirm.
When to use: Financial transactions, inventory management (don’t oversell), leader election, locking systems.
Cost: Higher latency. A write to a strongly consistent system spanning 3 data centers takes at least 1-2 network round trips between data centers.
Eventual Consistency
Given enough time without updates, all replicas converge. Stale reads are possible but temporary.
Implementation: Asynchronous replication. A write is acknowledged immediately and propagated in the background.
When to use: DNS records, social media feeds, CDN caches, product catalogs, any scenario where slight staleness is acceptable.
Cost: Risk of serving stale data. Conflict resolution is needed if concurrent writes happen on different nodes.
Causal Consistency
Preserves cause and effect. If operation A causes operation B, all nodes see A before B. Concurrent operations (neither caused the other) can be seen in any order.
Implementation: Uses vector clocks to track causality.
When to use: Collaborative editing (Google Docs style), social media comments (a reply should appear after the original post), user sessions.
# Vector clock implementation for causal consistency
class VectorClock:
def __init__(self, node_id: str):
self.node_id = node_id
self.clock = {}
def tick(self):
self.clock[self.node_id] = self.clock.get(self.node_id, 0) + 1
def update(self, other_clock: dict):
for node, ts in other_clock.items():
self.clock[node] = max(self.clock.get(node, 0), ts)
def happens_before(self, other: 'VectorClock') -> bool:
"""Check if this clock happens before another"""
for node, ts in self.clock.items():
if ts > other.clock.get(node, 0):
return False
return any(ts < other.clock.get(node, 0) for node, ts in self.clock.items())
def concurrent(self, other: 'VectorClock') -> bool:
return not self.happens_before(other) and not other.happens_before(self)
# Example
alice = VectorClock("alice")
bob = VectorClock("bob")
alice.tick() # Alice edits document
print(f"Alice's clock: {alice.clock}")
bob.update(alice.clock) # Bob receives Alice's update
bob.tick() # Bob makes his edit
print(f"Bob's clock: {bob.clock}")
print(f"Alice happens before Bob? {alice.happens_before(bob)}")
print(f"Concurrent? {alice.concurrent(bob)}")Expected output:
Alice's clock: {'alice': 1}
Bob's clock: {'alice': 1, 'bob': 1}
Alice happens before Bob? True
Concurrent? FalseRead-Your-Writes Consistency
After a client writes, its subsequent reads always see that write. Other clients may not see it yet.
Implementation: Track the timestamp of the client’s last write and ensure reads go to a node with that timestamp or later.
When to use: User profile updates, account settings. The user who changed their password should immediately see the change when they reload the page.
CRDTs (Conflict-free Replicated Data Types)
CRDTs are data structures that automatically resolve conflicts without coordination. Two replicas can be updated independently and merged automatically.
# Grow-only Counter (G-Counter) — a simple CRDT
class GCounter:
def __init__(self, node_id: str):
self.node_id = node_id
self.counts = {} # node_id -> count
def increment(self):
self.counts[self.node_id] = self.counts.get(self.node_id, 0) + 1
def value(self) -> int:
return sum(self.counts.values())
def merge(self, other: 'GCounter'):
for node, count in other.counts.items():
self.counts[node] = max(self.counts.get(node, 0), count)
# Two replicas
replica_a = GCounter("node-a")
replica_b = GCounter("node-b")
replica_a.increment() # {node-a: 1}
replica_a.increment() # {node-a: 2}
replica_b.increment() # {node-b: 1}
# Network partition heals — merge
replica_a.merge(replica_b)
replica_b.merge(replica_a)
print(f"Replica A value: {replica_a.value()}")
print(f"Replica B value: {replica_b.value()}")Expected output:
Replica A value: 3
Replica B value: 3Choosing the Right Model
| Requirement | Recommended Model | Example |
|---|---|---|
| Financial accuracy | Strong | Payment processing |
| High availability > freshness | Eventual | Social media likes |
| User expects own data immediately | Read-your-writes | Profile settings |
| Collaborative without conflicts | CRDTs | Google Docs |
| Causality matters, latency doesn’t | Causal | Comment threads |
Common Mistakes
Using strong consistency everywhere: It’s expensive and slow. Use the weakest model that meets your correctness requirements.
Ignoring client-side consistency: Read-your-writes can be implemented at the client (track timestamps) without server-side strong consistency.
Not testing what happens during partitions: In an eventually consistent system, test: what does the user see during a 5-second, 30-second, 5-minute partition?
Overlooking concurrent write conflicts: Without CRDTs or consensus, concurrent writes can cause conflicts that must be resolved (last-write-wins, custom merge, or manual resolution).
Assuming monotonic reads: Without careful configuration, a client might read an older value after reading a newer one (non-monotonic). Use consistent routing or client-side timestamps.
Practice Questions
What is the tradeoff between strong and eventual consistency? Strong consistency guarantees fresh reads but adds latency (need majority confirmation). Eventual consistency reduces latency but allows temporary stale reads.
How do CRDTs resolve conflicts? CRDTs use mathematical properties (monotonicity, commutativity) so all replicas converge to the same state when merged, regardless of order.
What is causal consistency in simple terms? If event A causes event B, everyone sees A before B. Events that didn’t cause each other can be seen in any order.
When would you use read-your-writes consistency? When a user should immediately see their own changes (profile update, password change, comment they just posted), even if other users don’t see it yet.
Why is strong consistency expensive in distributed systems? It requires all or majority of nodes to agree on each write before responding, which takes network round trips. For data centers on different continents, this adds hundreds of milliseconds.
Mini Project
Build a CRDT-based collaborative counter:
import copy
class PNCounter: # Positive-Negative Counter (supports increment and decrement)
def __init__(self, node_id: str):
self.node_id = node_id
self.pos = {} # Positive counts per node
self.neg = {} # Negative counts per node
def increment(self):
self.pos[self.node_id] = self.pos.get(self.node_id, 0) + 1
def decrement(self):
self.neg[self.node_id] = self.neg.get(self.node_id, 0) + 1
def value(self) -> int:
return sum(self.pos.values()) - sum(self.neg.values())
def merge(self, other: 'PNCounter'):
for node, count in other.pos.items():
self.pos[node] = max(self.pos.get(node, 0), count)
for node, count in other.neg.items():
self.neg[node] = max(self.neg.get(node, 0), count)
# Simulate two nodes with concurrent operations
n1 = PNCounter("node-1")
n2 = PNCounter("node-2")
n1.increment() # +1
n1.increment() # +1
n2.decrement() # -1 (concurrent)
n1.merge(n2)
n2.merge(n1)
print(f"Final value: {n1.value()}") # 2 + (-1) = 1Expected output:
Final value: 1Cross-References
- Distributed Systems
- System Design Overview
- Database Sharding
- Caching
- Event-Driven Architecture
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro