Preface#
Last week, you mastered caching. You learned to cache aggressively, invalidate strategically, and serve stale data for performance. You accepted "eventual consistency" as a trade-off.
But what does "consistent" actually mean?
- Developer A: "Our system is eventually consistent."
- Developer B: "What does that mean exactly?"
- Developer A: "Eventually, all replicas will have the same data."
- Developer B: "How long is 'eventually'? Seconds? Hours?"
- Developer A: "Uh... it depends?"
- Developer C: "We need strong consistency for this feature."
- Developer D: "What's the difference from eventual?"
- Developer C: "Strong means... stronger? More consistent?"
- Developer D: "That's not helpful."
The problem: "Consistency" means different things in different contexts, and most engineers can't articulate the precise guarantees they need.
Today, we'll fix that confusion. You'll learn:
- The precise definitions of consistency models
- When each model is appropriate
- How to implement systems with specific consistency guarantees
- The real meaning of CAP theorem
Consistency answers who sees a write, and when: if A writes X=1 on node 1, a read on node 2 may see 1, the old 0, or an error. The spectrum runs from linearizable (one copy) through sequential, causal (a comment never before its post), read-your-writes and monotonic reads to eventual (DNS can take 48 hours). During a partition CP systems refuse and AP systems answer stale; quorums with W + R > N, and choosing per operation (eventual for browsing, strong for checkout, read-your-writes for order history), let one system mix them.
Part I: Foundations#
Chapter 1: What Is Consistency?#
1.1 The Core Question#
Consistency answers a simple question: When a write happens, who sees it and when?
Timeline
- T=0: User A writes X=1 to Node 1
- T=1: User B reads X from Node 2
Question: What does User B see?
Possible answers
| User B sees | Model | |
|---|---|---|
| A | X=1 (the new value) | Strong consistency |
| B | X=0 (the old value) | Eventual consistency (temporarily) |
| C | Error ("data unavailable") | Choosing consistency over availability |
| D | It depends on other factors | Causal consistency |
1.2 Why Distributed Systems Make This Hard#
Single machine
- Memory holds
X = 1 - Write and Read both go to that one copy (instant visibility)
Write happens, read sees it. Simple.
Distributed system
- Node 1 holds
X = 1— Write at T=0 - Update travels to Node 2 over the network (delay)
- Node 2 holds
X = ?— Read at T=1
Write happens on Node 1. Read happens on Node 2. Network delay means Node 2 might not have the update yet. What does the read return?
1.3 The Fundamental Trade-off#
You cannot have all three simultaneously:
- Instant visibility (strong consistency)
- Low latency (fast responses)
- High availability (always responds)
Three corners: Instant Visibility · Low Latency · High Availability
Pick any two
- Instant + Low Latency → Sacrifice availability (reject reads during replication)
- Instant + High Availability → Sacrifice latency (wait for all replicas)
- Low Latency + High Availability → Sacrifice instant visibility (eventual consistency)
Chapter 2: The Consistency Spectrum#
2.1 Overview of Consistency Models#
- Linearizability (Strongest) — "Behaves like a single copy"
- Every operation appears to happen atomically at some point between invocation and response
- Sequential consistency — "All nodes see the same order"
- Operations appear in some single total order that respects each client's own program order (but not necessarily real time)
- Causal consistency — "Cause before effect"
- If operation A causally precedes B, everyone sees A before B
- Concurrent operations may be seen in different orders
- Read-your-writes — "See your own updates"
- A client always sees its own previous writes
- May not see other clients' writes immediately
- Monotonic reads — "Never go backwards"
- Once you read a value, you never read an older value
- Eventual consistency (Weakest) — "Eventually converges"
- If no new writes, all replicas will eventually have same value
- No guarantees about when or what you see before convergence
2.2 Linearizability (Strong Consistency)#
The gold standard. The system behaves as if there's only one copy of the data.
Requirement: Every operation appears to take effect atomically at some point between its start and end.
Example (time flows left → right)
- Client A:
Write(X=1)runs first; a laterRead(X)that starts after the write has finished → Must return 1 - Client B:
Read(X)overlapsWrite(X=1)→ could return 0 or 1, depending on whereWrite(X=1)takes effect
If B's read returns 1, then any read that STARTS after B's read COMPLETES must also return 1 (or a newer value).
Real-world analogy
Imagine a global clock visible to everyone. Each operation gets a timestamp. Everyone agrees on the order based on timestamps.
When to use:
- Financial transactions
- Inventory reservations
- Unique constraint enforcement
- Leader election
Cost:
- Higher latency (must coordinate)
- Lower availability (rejects operations during partitions)
2.3 Sequential Consistency#
Weaker than linearizable, but still provides a global order.
Requirement: All nodes see operations in the SAME order, and each client's own operations appear in the order that client issued them, but that order doesn't have to match real-time.
Example
Real time:
- Client A:
Write(X=1)at T=0 - Client B:
Write(X=2)at T=1
Sequential consistency allows: all nodes see Write(X=2), then Write(X=1) — as long as ALL nodes agree on this order.
Real-world analogy
Like a message board where posts appear in order, but the order might not match when they were written. Everyone sees the same order of posts.
Difference from linearizability
| Guarantee | |
|---|---|
| Linearizable | If Write(X=1) finishes before Write(X=2) starts, everyone must see X=1 before X=2 |
| Sequential | Writes from different clients might appear in any order, as long as everyone agrees (each client's own operations keep their program order) |
2.4 Causal Consistency#
Respects cause-and-effect relationships.
Requirement: If operation A "happens before" B (causally), everyone sees A before B. Concurrent operations can be in any order.
Example: Social Media Comments
- Alice posts: "I'm getting married!" (Post P1)
- Bob comments: "Congratulations!" (Comment C1 on P1)
C1 is causally dependent on P1 (Bob saw the post, then commented). Causal consistency guarantees: everyone sees P1 before C1.
- Carol posts: "Nice weather today" (Post P2, concurrent with P1)
P2 is concurrent with P1 (no causal relationship). Causal consistency allows:
- ok — Some users see P1, P2, C1
- ok — Some users see P2, P1, C1
- no — No user sees C1 before P1
How it works
- Track causal dependencies (vector clocks, version vectors)
- Delay applying updates until their dependencies are satisfied
When to use:
- Social media feeds
- Collaborative applications
- Chat systems
- Any system where cause-effect matters but global order doesn't
2.5 Read-Your-Writes#
A client always sees its own updates.
Requirement: If a client writes X, subsequent reads by that SAME CLIENT will see X (or a later value).
Example: Profile Update
User updates profile picture, then refreshes page.
| Without read-your-writes | With read-your-writes |
|---|---|
| User might see old picture (read went to stale replica) | User always sees their new picture |
| User thinks: "Did my update work? Let me try again..." | Confidence in the system |
Implementation strategies
- Sticky sessions: Route user to same replica
- Write to leader, read from leader (for that user)
- Track write timestamp, reject stale reads
2.6 Monotonic Reads#
Never go backwards in time.
Requirement: Once a client reads value V, it never reads a value older than V.
Example: Reading a Counter
Counter value over time: 5 → 6 → 7 → 8
| Without monotonic reads | With monotonic reads |
|---|---|
| Read 1: Returns 7 (from replica A) | Read 1: Returns 7 |
| Read 2: Returns 5 (from stale replica B) | Read 2: Returns 7, 8, or newer (never 5 or 6) |
| User thinks the counter went backwards! |
Implementation
- Track the "version" of last read
- Only accept reads from replicas with >= that version
2.7 Eventual Consistency#
The weakest guarantee, but the most available.
Requirement: If no new writes occur, all replicas will EVENTUALLY converge to the same value.
What it DOESN'T guarantee
- When convergence happens
- What you see before convergence
- Read-your-writes
- Monotonic reads
- Causal ordering
Example: DNS
You update your domain's IP address. Propagation time: "up to 48 hours". Different users see different IPs during propagation. Eventually, everyone sees the new IP.
When it's acceptable
- Caching (Week 4!)
- View counts, likes
- Analytics data
- Non-critical reads
2.8 Comparison Table#
| Model | Guarantee | Latency | Availability | Use Case |
|---|---|---|---|---|
| Linearizable | Single-copy behavior | High | Low | Transactions, locks |
| Sequential | Global order agreed | Medium | Medium | Logs, message queues |
| Causal | Cause before effect | Low | High | Social, collaboration |
| Read-your-writes | See own updates | Low | High | User profiles |
| Monotonic reads | Never go back | Low | High | Counters, feeds |
| Eventual | Eventually same | Lowest | Highest | Caching, analytics |
Chapter 3: CAP Theorem in Practice#
3.1 The Theorem (Correctly Stated)#
In a distributed system experiencing a network PARTITION, you must choose between CONSISTENCY and AVAILABILITY.
- C - Consistency: All nodes see the same data at the same time (formally: linearizability)
- A - Availability: Every request to a non-failing node receives a (non-error) response
- P - Partition Tolerance: System continues despite network failures
Key insight: P is not optional. Networks WILL partition. So the choice is really: CP or AP during partitions.
| During NORMAL operation (no partition) | During PARTITION |
|---|---|
| You CAN have both C and A! | Choose C: Reject requests to maintain consistency |
| Choose A: Allow stale reads/writes to maintain availability |
A partition cuts two of five nodes off
A network partition splits a five-node cluster into a group of three and a group of two. A user whose requests land on the two-node side tries to reserve the last unit of stock. What does a CP system answer on that side, and what does an AP system answer? What does each choice cost you?
One strong answer
- CP: the two-node side is the minority, so it rejects the reservation; the three-node majority keeps serving with consistency. Cost: an error for that user until the partition heals.
- AP: both sides keep accepting writes, so the same last unit can be sold on each side. Cost: diverging data that must be reconciled later (last-write-wins, CRDTs, or a human apology).
- For stock: a wrong reservation is worse than a refusal, so this is a CP operation.
3.2 CP vs AP: Real Examples#
CP systems (Consistency over Availability)
Examples
- Traditional RDBMS with sync replication
- ZooKeeper, etcd (coordination services)
- Google Spanner (uses GPS + atomic clocks)
Behavior during partition
- Minority partition becomes unavailable
- Majority partition continues with consistency
- "It's better to be unavailable than wrong"
Use for
- Financial systems
- Inventory management
- Leader election
- Anything where incorrect data is worse than no data
AP systems (Availability over Consistency)
Examples
- Cassandra (by default)
- DynamoDB (eventually consistent reads)
- DNS
- Caching layers
Behavior during partition
- All nodes continue accepting reads/writes
- Different nodes may have different data
- Conflicts resolved later (last-write-wins, CRDTs, etc.)
Use for
- Shopping carts
- User sessions
- View counts
- Anything where availability is critical
One consistency level for the whole store?
An online store reads and writes through one database: browsing products, showing "3 left" on a product page, reserving stock, checking out and viewing order history. A teammate proposes one consistency level for all of it, to keep things simple. Strong everywhere, or eventual everywhere? Or something else?
One strong answer
- Neither. Strong everywhere puts every browse on the coordinated path, paying latency and availability for reads nobody would notice were stale. Eventual everywhere lets reservations and checkout act on stale stock and oversell.
- Choose per operation: eventual for browsing and the displayed count, strong for reserving and checkout, read-your-writes for order history (users notice their own missing order).
3.3 The Modern View: Tunable Consistency#
Modern databases don't force a single choice. You can choose consistency per-operation.
DynamoDB
- Eventually consistent reads (default, fast, cheap)
- Strongly consistent reads (option, slower, costlier)
Cassandra
Consistency level per query:
- ONE: Read/write to any single replica (fast, weak)
- QUORUM: Read/write to majority (balanced)
- ALL: Read/write to all replicas (slow, strong)
Your application
| Operation | Consistency |
|---|---|
| Browse products | Eventually consistent (cache hit) |
| Check inventory for display | Eventually consistent |
| Reserve inventory | Strongly consistent |
| Checkout | Strongly consistent |
| View order history | Read-your-writes |
Chapter 4: Implementing Consistency#
4.1 Implementing Read-Your-Writes#
# Read-Your-Writes Implementation
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Any
import asyncio
import time
@dataclass
class WriteRecord:
"""Track a client's last write."""
key: str
timestamp: float
replica_id: str
class ReadYourWritesClient:
"""
Client that guarantees read-your-writes consistency.
Tracks writes and ensures subsequent reads see them.
"""
def __init__(self, replicas: list, session_id: str):
self.replicas = replicas
self.session_id = session_id
self.last_writes: dict[str, WriteRecord] = {}
async def write(self, key: str, value: Any) -> bool:
"""Write to primary and track the write."""
primary = self._get_primary()
timestamp = time.time()
success = await primary.write(key, value, timestamp)
if success:
# Track this write for read-your-writes
self.last_writes[key] = WriteRecord(
key=key,
timestamp=timestamp,
replica_id=primary.id
)
return success
async def read(self, key: str) -> Any:
"""Read with read-your-writes guarantee."""
last_write = self.last_writes.get(key)
if last_write:
# We wrote this key - ensure we see our write
return await self._read_with_min_version(key, last_write.timestamp)
else:
# We didn't write this key - any replica is fine
return await self._read_from_any(key)
async def _read_with_min_version(self, key: str, min_timestamp: float) -> Any:
"""Read ensuring we see at least version min_timestamp."""
# Strategy 1: Read from the replica we wrote to
for replica in self.replicas:
if replica.id == self.last_writes[key].replica_id:
return await replica.read(key)
# Strategy 2: Query replicas until we find one with our version
for replica in self.replicas:
value, timestamp = await replica.read_with_version(key)
if timestamp >= min_timestamp:
return value
# Strategy 3: Wait for replication (with timeout)
return await self._wait_for_replication(key, min_timestamp)
async def _wait_for_replication(
self,
key: str,
min_timestamp: float,
timeout: float = 5.0
) -> Any:
"""Wait for a replica to catch up."""
deadline = time.time() + timeout
while time.time() < deadline:
for replica in self.replicas:
value, timestamp = await replica.read_with_version(key)
if timestamp >= min_timestamp:
return value
await asyncio.sleep(0.1)
raise TimeoutError(f"Replica did not catch up within {timeout}s")
async def _read_from_any(self, key: str) -> Any:
"""Read from any available replica."""
for replica in self.replicas:
try:
return await replica.read(key)
except Exception:
continue
raise Exception("All replicas unavailable")
def _get_primary(self):
"""Get the primary replica for writes."""
return self.replicas[0] # SimplifiedThe counter that went from 7 to 5
A client polls a counter, and each poll hits a randomly chosen replica. It reads 7, then 5. Without making every read go to the primary, what would you store on the client, and what must each replica return, so the client never goes backwards?
One strong answer
- On the client: the highest version it has seen for each key.
- From each replica: the value together with its version.
- The rule: accept a reply only if its version is at least the one remembered, then raise the remembered version; skip a staler replica and try another, falling back to the primary, which always has the latest. Reads still spread across replicas, but never go backwards.
4.2 Implementing Monotonic Reads#
# Monotonic Reads Implementation
@dataclass
class VersionedValue:
"""Value with version for comparison."""
value: Any
version: int # Logical clock or timestamp
class MonotonicReadsClient:
"""
Client that guarantees monotonic reads.
Once you read version N, you never read version < N.
"""
def __init__(self, replicas: list, session_id: str):
self.replicas = replicas
self.session_id = session_id
# Track minimum version seen per key
self.min_versions: dict[str, int] = {}
async def read(self, key: str) -> Any:
"""Read with monotonic guarantee."""
min_version = self.min_versions.get(key, 0)
# Try replicas until we find one with sufficient version
for replica in self._shuffled_replicas():
try:
result = await replica.read_versioned(key)
if result.version >= min_version:
# Update our minimum version
self.min_versions[key] = result.version
return result.value
else:
# This replica is stale, try another
continue
except Exception:
continue
# All replicas are stale or unavailable
raise Exception(f"No replica has version >= {min_version}")
def _shuffled_replicas(self):
"""Shuffle replicas for load balancing."""
import random
replicas = self.replicas.copy()
random.shuffle(replicas)
return replicas
# Combined: Read-Your-Writes + Monotonic Reads
class SessionConsistencyClient:
"""
Combines read-your-writes and monotonic reads.
This is what most users expect from a "consistent" system.
"""
def __init__(self, replicas: list, session_id: str):
self.replicas = replicas
self.session_id = session_id
# Track writes and read versions
self.write_versions: dict[str, int] = {}
self.read_versions: dict[str, int] = {}
async def write(self, key: str, value: Any) -> int:
"""Write and track version."""
primary = self.replicas[0]
version = await primary.write(key, value)
self.write_versions[key] = version
self.read_versions[key] = max(
self.read_versions.get(key, 0),
version
)
return version
async def read(self, key: str) -> Any:
"""Read with session consistency."""
# Minimum version is max of:
# - Our last write version
# - Our last read version
min_version = max(
self.write_versions.get(key, 0),
self.read_versions.get(key, 0)
)
for replica in self.replicas:
result = await replica.read_versioned(key)
if result.version >= min_version:
self.read_versions[key] = result.version
return result.value
raise Exception("Consistency requirement not satisfiable")4.3 Implementing Causal Consistency#
# Causal Consistency with Vector Clocks
from dataclasses import dataclass, field
from typing import Dict, Any, Optional
import uuid
@dataclass
class VectorClock:
"""
Vector clock for tracking causal dependencies.
Each node maintains a counter. The vector clock
is the collection of all node counters.
"""
clocks: Dict[str, int] = field(default_factory=dict)
def increment(self, node_id: str):
"""Increment this node's clock (on local event)."""
self.clocks[node_id] = self.clocks.get(node_id, 0) + 1
def merge(self, other: 'VectorClock'):
"""Merge with another vector clock (on receive)."""
for node_id, count in other.clocks.items():
self.clocks[node_id] = max(
self.clocks.get(node_id, 0),
count
)
def happens_before(self, other: 'VectorClock') -> bool:
"""Check if self happened before other."""
# self < other if:
# - All of self's clocks are <= other's
# - At least one is strictly <
all_leq = all(
self.clocks.get(node, 0) <= other.clocks.get(node, 0)
for node in set(self.clocks) | set(other.clocks)
)
some_lt = any(
self.clocks.get(node, 0) < other.clocks.get(node, 0)
for node in set(self.clocks) | set(other.clocks)
)
return all_leq and some_lt
def concurrent_with(self, other: 'VectorClock') -> bool:
"""Check if self and other are concurrent."""
return not self.happens_before(other) and not other.happens_before(self)
def copy(self) -> 'VectorClock':
return VectorClock(clocks=self.clocks.copy())
@dataclass
class CausalMessage:
"""Message with causal metadata."""
key: str
value: Any
vector_clock: VectorClock
dependencies: list # List of message IDs this depends on
msg_id: str = field(default_factory=lambda: str(uuid.uuid4())) # Stable across nodes
class CausalConsistencyNode:
"""
Node implementing causal consistency.
Delays applying updates until their causal
dependencies are satisfied.
"""
def __init__(self, node_id: str):
self.node_id = node_id
self.vector_clock = VectorClock()
self.data: Dict[str, Any] = {}
self.pending: list[CausalMessage] = []
self.delivered: set = set() # Message IDs we've applied
def local_write(self, key: str, value: Any) -> CausalMessage:
"""Perform a local write operation."""
# Increment our clock
self.vector_clock.increment(self.node_id)
# Create message with current vector clock
msg = CausalMessage(
key=key,
value=value,
vector_clock=self.vector_clock.copy(),
dependencies=list(self.delivered)
)
# Apply locally
self.data[key] = value
self.delivered.add(msg.msg_id)
return msg # Broadcast this to other nodes
def receive_message(self, msg: CausalMessage):
"""Receive a message from another node."""
if self._dependencies_satisfied(msg):
self._apply_message(msg)
self._check_pending()
else:
# Buffer until dependencies are satisfied
self.pending.append(msg)
def _dependencies_satisfied(self, msg: CausalMessage) -> bool:
"""Check if all dependencies have been delivered."""
return all(dep in self.delivered for dep in msg.dependencies)
def _apply_message(self, msg: CausalMessage):
"""Apply a message to local state."""
self.data[msg.key] = msg.value
self.vector_clock.merge(msg.vector_clock)
self.delivered.add(msg.msg_id)
def _check_pending(self):
"""Check if any pending messages can now be applied."""
made_progress = True
while made_progress:
made_progress = False
for msg in list(self.pending):
if self._dependencies_satisfied(msg):
self._apply_message(msg)
self.pending.remove(msg)
made_progress = True
# Example usage: Causal social media
class CausalSocialFeed:
"""
Social feed with causal consistency.
Guarantees: Comments always appear after their parent posts.
"""
def __init__(self, user_id: str, node: CausalConsistencyNode):
self.user_id = user_id
self.node = node
def create_post(self, content: str) -> str:
"""Create a new post."""
post_id = generate_id()
msg = self.node.local_write(
key=f"post:{post_id}",
value={
"id": post_id,
"author": self.user_id,
"content": content,
"type": "post"
}
)
# Broadcast msg to other nodes
return post_id
def comment_on_post(self, post_id: str, content: str) -> str:
"""Comment on an existing post."""
comment_id = generate_id()
# The comment causally depends on the post
# (We saw the post, so our vector clock includes it)
msg = self.node.local_write(
key=f"comment:{comment_id}",
value={
"id": comment_id,
"parent_post": post_id,
"author": self.user_id,
"content": content,
"type": "comment"
}
)
# When other nodes receive this comment,
# they won't display it until they've seen the parent post
return comment_idConsistency models and CAP
Q1/3 A user updates their profile picture, refreshes, and sees the old one because the read hit a lagging replica. What is the weakest guarantee that fixes this?
Answer & explanation
Answer: C
The user only needs to see their own write, which is exactly read-your-writes (sticky routing, read from leader, or a min-version check). Linearizability would also fix it but forces coordination on every read; monotonic reads only stops values going backwards, it does not promise you see your write.
Q2/3 Alice posts P1, Bob comments C1 on it, and Carol posts an unrelated P2 at the same time. Under causal consistency, which observation is NOT allowed?
Answer & explanation
Answer: B
C1 causally depends on P1, so everyone must see P1 first; updates are held back until their dependencies are delivered. P2 is concurrent with P1, so different users may legitimately see those two in different orders.
Q3/3 According to the practical view of CAP, when are you actually forced to choose between consistency and availability?
Answer & explanation
Answer: D
Partitions will happen, so P is not optional; the C-or-A choice applies during the partition, and without one you can have both. "Choose CA" is a misconception because you cannot opt out of network failures in a distributed system.
Part II: Production Implementation#
Chapter 5: Consistency-Aware Data Access Layer#
Note: the samples call fetch_one, as in the databases library (or a thin wrapper with that method). With asyncpg the equivalent is fetchrow, which returns a Record.
# Production Consistency-Aware Data Access Layer
import asyncio
from dataclasses import dataclass
from typing import Any, Optional, List, Callable
from enum import Enum
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class ConsistencyLevel(Enum):
"""Consistency levels for operations."""
EVENTUAL = "eventual"
# Read from any replica, may be stale
# Fastest, highest availability
MONOTONIC = "monotonic"
# Never read older value than previously seen
# Requires tracking read versions per session
READ_YOUR_WRITES = "read_your_writes"
# Always see your own writes
# Requires tracking write versions per session
SESSION = "session"
# Combination of monotonic + read-your-writes
# What users typically expect
CAUSAL = "causal"
# Respects causal ordering
# Requires vector clocks or dependency tracking
STRONG = "strong"
# Linearizable, single-copy semantics
# Slowest, may be unavailable during partitions
@dataclass
class ConsistencyContext:
"""Context for tracking consistency requirements."""
session_id: str
write_versions: dict = None
read_versions: dict = None
vector_clock: dict = None
def __post_init__(self):
self.write_versions = self.write_versions or {}
self.read_versions = self.read_versions or {}
self.vector_clock = self.vector_clock or {}
class ConsistencyAwareRepository:
"""
Repository with configurable consistency levels.
Allows different operations to use different consistency
levels based on their requirements.
"""
def __init__(
self,
primary_db, # Primary database connection
read_replicas: list, # Read replica connections
cache, # Cache layer (Redis)
default_level: ConsistencyLevel = ConsistencyLevel.SESSION
):
self.primary = primary_db
self.replicas = read_replicas
self.cache = cache
self.default_level = default_level
async def read(
self,
key: str,
consistency: ConsistencyLevel = None,
context: ConsistencyContext = None
) -> Optional[Any]:
"""
Read with specified consistency level.
"""
level = consistency or self.default_level
context = context or ConsistencyContext(session_id="default")
if level == ConsistencyLevel.EVENTUAL:
return await self._read_eventual(key)
elif level == ConsistencyLevel.MONOTONIC:
return await self._read_monotonic(key, context)
elif level == ConsistencyLevel.READ_YOUR_WRITES:
return await self._read_your_writes(key, context)
elif level == ConsistencyLevel.SESSION:
return await self._read_session(key, context)
elif level == ConsistencyLevel.STRONG:
return await self._read_strong(key)
else:
raise ValueError(f"Unknown consistency level: {level}")
async def write(
self,
key: str,
value: Any,
consistency: ConsistencyLevel = None,
context: ConsistencyContext = None
) -> int:
"""
Write with specified consistency level.
Returns version number of the write.
"""
level = consistency or self.default_level
context = context or ConsistencyContext(session_id="default")
if level == ConsistencyLevel.STRONG:
version = await self._write_strong(key, value)
else:
version = await self._write_async(key, value)
# Update context for read-your-writes
context.write_versions[key] = version
context.read_versions[key] = max(
context.read_versions.get(key, 0),
version
)
return version
# =========================================================================
# Eventual Consistency
# =========================================================================
async def _read_eventual(self, key: str) -> Optional[Any]:
"""Read from cache or any replica. May be stale."""
# Try cache first
cached = await self.cache.get(key)
if cached is not None:
return cached
# Try replicas (load balanced)
import random
replicas = self.replicas.copy()
random.shuffle(replicas)
for replica in replicas:
try:
result = await replica.fetch_one(
"SELECT value, version FROM data WHERE key = $1",
key
)
if result:
# Cache for future reads
await self.cache.set(key, result['value'], ttl=60)
return result['value']
except Exception as e:
logger.warning(f"Replica read failed: {e}")
continue
# Fall back to primary
return await self._read_from_primary(key)
# =========================================================================
# Monotonic Reads
# =========================================================================
async def _read_monotonic(
self,
key: str,
context: ConsistencyContext
) -> Optional[Any]:
"""Read ensuring we never go backwards."""
min_version = context.read_versions.get(key, 0)
# Try replicas that have sufficient version
for replica in self.replicas:
try:
result = await replica.fetch_one(
"""
SELECT value, version FROM data
WHERE key = $1 AND version >= $2
""",
key, min_version
)
if result:
context.read_versions[key] = result['version']
return result['value']
except Exception:
continue
# Fall back to primary (always has latest)
result = await self._read_from_primary_versioned(key)
if result and result['version'] >= min_version:
context.read_versions[key] = result['version']
return result['value']
return None
# =========================================================================
# Read-Your-Writes
# =========================================================================
async def _read_your_writes(
self,
key: str,
context: ConsistencyContext
) -> Optional[Any]:
"""Read ensuring we see our own writes."""
write_version = context.write_versions.get(key)
if write_version is None:
# We haven't written this key, any replica is fine
return await self._read_eventual(key)
# We wrote this key, ensure we see our write
return await self._read_with_min_version(key, write_version)
async def _read_with_min_version(
self,
key: str,
min_version: int,
context: ConsistencyContext = None
) -> Optional[Any]:
"""Read from replica with at least min_version."""
# Try replicas
for replica in self.replicas:
result = await replica.fetch_one(
"""
SELECT value, version FROM data
WHERE key = $1 AND version >= $2
""",
key, min_version
)
if result:
break
else:
# Replicas don't have our version yet, use primary
result = await self._read_from_primary_versioned(key)
if result and context:
# Record the version actually seen (for monotonic reads)
context.read_versions[key] = result['version']
return result['value'] if result else None
# =========================================================================
# Session Consistency (Monotonic + Read-Your-Writes)
# =========================================================================
async def _read_session(
self,
key: str,
context: ConsistencyContext
) -> Optional[Any]:
"""Read with session consistency guarantees."""
# Min version is max of write version and read version
min_version = max(
context.write_versions.get(key, 0),
context.read_versions.get(key, 0)
)
if min_version == 0:
return await self._read_eventual(key)
# Records the version actually read, so later reads never go backwards
return await self._read_with_min_version(key, min_version, context)
# =========================================================================
# Strong Consistency
# =========================================================================
async def _read_strong(self, key: str) -> Optional[Any]:
"""Read from primary only. Linearizable."""
return await self._read_from_primary(key)
async def _write_strong(self, key: str, value: Any) -> int:
"""Write with synchronous replication."""
async with self.primary.transaction():
# Write to primary
result = await self.primary.fetch_one(
"""
INSERT INTO data (key, value, version)
VALUES ($1, $2, 1)
ON CONFLICT (key) DO UPDATE
SET value = $2, version = data.version + 1
RETURNING version
""",
key, value
)
version = result['version']
# Synchronously replicate to all replicas
# (This is slow but guarantees strong consistency)
await self._sync_replicate(key, value, version)
# Invalidate cache
await self.cache.delete(key)
return version
async def _write_async(self, key: str, value: Any) -> int:
"""Write to primary with async replication."""
result = await self.primary.fetch_one(
"""
INSERT INTO data (key, value, version)
VALUES ($1, $2, 1)
ON CONFLICT (key) DO UPDATE
SET value = $2, version = data.version + 1
RETURNING version
""",
key, value
)
version = result['version']
# Async replication (don't wait)
asyncio.create_task(self._async_replicate(key, value, version))
# Invalidate cache
await self.cache.delete(key)
return version
# =========================================================================
# Helper Methods
# =========================================================================
async def _read_from_primary(self, key: str) -> Optional[Any]:
"""Read directly from primary."""
result = await self.primary.fetch_one(
"SELECT value FROM data WHERE key = $1",
key
)
return result['value'] if result else None
async def _read_from_primary_versioned(self, key: str) -> Optional[dict]:
"""Read with version from primary."""
return await self.primary.fetch_one(
"SELECT value, version FROM data WHERE key = $1",
key
)
async def _sync_replicate(self, key: str, value: Any, version: int):
"""Synchronously replicate to all replicas."""
await asyncio.gather(*[
replica.execute(
"""
INSERT INTO data (key, value, version) VALUES ($1, $2, $3)
ON CONFLICT (key) DO UPDATE SET value = $2, version = $3
WHERE data.version < $3
""",
key, value, version
)
for replica in self.replicas
])
async def _async_replicate(self, key: str, value: Any, version: int):
"""Asynchronously replicate to replicas."""
for replica in self.replicas:
try:
await replica.execute(
"""
INSERT INTO data (key, value, version) VALUES ($1, $2, $3)
ON CONFLICT (key) DO UPDATE SET value = $2, version = $3
WHERE data.version < $3
""",
key, value, version
)
except Exception as e:
logger.warning(f"Async replication failed: {e}")Chapter 6: Real-World Consistency Patterns#
6.1 The Inventory Pattern#
# Inventory with Appropriate Consistency Levels
class InventoryService:
"""
Inventory service with consistency-aware operations.
Different operations have different consistency requirements:
- Display: Eventual consistency (from cache)
- Reservation: Strong consistency (prevent oversell)
- Release: Strong consistency (prevent under-count)
"""
def __init__(
self,
repository: ConsistencyAwareRepository,
cache
):
self.repo = repository
self.cache = cache
async def get_display_count(self, product_id: str) -> int:
"""
Get inventory count for DISPLAY purposes.
Eventual consistency is fine - showing stale count
on a product page is acceptable.
"""
# Try cache first (eventual consistency)
cache_key = f"inventory:{product_id}"
cached = await self.cache.get(cache_key)
if cached is not None:
return cached
# Read from replica (eventual consistency)
count = await self.repo.read(
key=f"inventory:{product_id}",
consistency=ConsistencyLevel.EVENTUAL
)
# Cache for future reads
if count is not None:
await self.cache.set(cache_key, count, ttl=30)
return count or 0
async def reserve(
self,
product_id: str,
quantity: int,
reservation_id: str
) -> bool:
"""
Reserve inventory for checkout.
STRONG consistency required - we must prevent overselling.
Uses pessimistic locking (SELECT ... FOR UPDATE) on the primary.
"""
async with self.repo.primary.transaction(isolation="SERIALIZABLE"):
# Read current inventory with lock
result = await self.repo.primary.fetch_one(
"""
SELECT quantity, version FROM inventory
WHERE product_id = $1 FOR UPDATE
""",
product_id
)
if not result or result['quantity'] < quantity:
return False
# Decrement inventory
await self.repo.primary.execute(
"""
UPDATE inventory
SET quantity = quantity - $2, version = version + 1
WHERE product_id = $1
""",
product_id, quantity
)
# Record reservation
await self.repo.primary.execute(
"""
INSERT INTO reservations (id, product_id, quantity, status, created_at)
VALUES ($1, $2, $3, 'active', NOW())
""",
reservation_id, product_id, quantity
)
# Invalidate cache
await self.cache.delete(f"inventory:{product_id}")
return True
async def release(self, reservation_id: str) -> bool:
"""
Release a reservation (order cancelled).
STRONG consistency - must accurately restore inventory.
"""
async with self.repo.primary.transaction():
# Get reservation
reservation = await self.repo.primary.fetch_one(
"""
SELECT product_id, quantity, status FROM reservations
WHERE id = $1 FOR UPDATE
""",
reservation_id
)
if not reservation or reservation['status'] != 'active':
return False
# Restore inventory
await self.repo.primary.execute(
"""
UPDATE inventory
SET quantity = quantity + $2, version = version + 1
WHERE product_id = $1
""",
reservation['product_id'], reservation['quantity']
)
# Mark reservation as released
await self.repo.primary.execute(
"""
UPDATE reservations SET status = 'released' WHERE id = $1
""",
reservation_id
)
# Invalidate cache
await self.cache.delete(f"inventory:{reservation['product_id']}")
return True
async def confirm(self, reservation_id: str) -> bool:
"""
Confirm a reservation (order completed).
Changes reservation status, inventory already decremented.
"""
result = await self.repo.primary.execute(
"""
UPDATE reservations SET status = 'confirmed'
WHERE id = $1 AND status = 'active'
""",
reservation_id
)
return result.rowcount > 06.2 The Profile Pattern#
# User Profile with Read-Your-Writes
class ProfileService:
"""
User profile service with session consistency.
Users expect to see their own updates immediately.
Other users' updates can be eventually consistent.
"""
def __init__(self, repository: ConsistencyAwareRepository):
self.repo = repository
async def get_profile(
self,
user_id: str,
viewer_id: str,
context: ConsistencyContext
) -> dict:
"""
Get a user's profile.
If viewing your own profile: Read-your-writes
If viewing someone else's: Eventual consistency
"""
if user_id == viewer_id:
# Viewing own profile - see your updates
consistency = ConsistencyLevel.READ_YOUR_WRITES
else:
# Viewing someone else - eventual is fine
consistency = ConsistencyLevel.EVENTUAL
return await self.repo.read(
key=f"profile:{user_id}",
consistency=consistency,
context=context
)
async def update_profile(
self,
user_id: str,
updates: dict,
context: ConsistencyContext
) -> dict:
"""Update user's profile."""
# Merge with existing profile
current = await self.repo.read(
key=f"profile:{user_id}",
consistency=ConsistencyLevel.STRONG, # Get latest for merge
context=context
)
updated = {**(current or {}), **updates}
await self.repo.write(
key=f"profile:{user_id}",
value=updated,
context=context # Track for read-your-writes
)
return updatedConsistency-aware data access
Q1/3 The product page shows "3 left" from a 30-second cache, while
reserve()locks the row on the primary. A teammate wants the product page to read from the primary too, "to be safe". What is the best response?Answer & explanation
Answer: A
Overselling is prevented where stock is actually decremented, under a strong, locked transaction. A slightly stale display number is acceptable. The cache is invalidated on writes but replicas and other caches can still lag, and routing all reads to the primary is the "strong consistency everywhere" mistake.
Q2/3 A session-consistent client last wrote version 7 of a key, but every read replica is still at version 6. What does the session read in the repository do?
Answer & explanation
Answer: C
The session read requires a minimum version of max(last write, last read); when no replica qualifies it falls back to the primary, which always has the latest. Returning version 6 would break read-your-writes, which is half of what session consistency promises.
Q3/3 Why does
get_profileuse read-your-writes when you view your own profile but eventual consistency when you view someone else's?Answer & explanation
Answer: D
Consistency is chosen per operation by who can observe the staleness. Your own edits must be visible to you, while other people's data can lag cheaply. The extra guarantee costs routing to fresher replicas or the primary, so you only pay it where a user would notice.
Part III: Real-World Application#
Chapter 7: Case Studies#
7.1 Case Study: Amazon DynamoDB#
DynamoDB offers tunable consistency per-read:
| Eventually consistent reads (Default) | Strongly consistent reads | |
|---|---|---|
| Reads from | Any replica | Leader replica |
| Freshness | May return stale data | Always returns latest data |
| Performance | Lower latency, higher throughput | Higher latency |
| Cost / availability | 50% cheaper than strongly consistent | May be unavailable during leader election |
| Use for | Product catalog, user preferences | Inventory, financial data |
Example API usage
# Eventually consistent (default)
response = dynamodb.get_item(
TableName='Products',
Key={'product_id': {'S': '12345'}}
)
# Strongly consistent
response = dynamodb.get_item(
TableName='Products',
Key={'product_id': {'S': '12345'}},
ConsistentRead=True
)Key insight: DynamoDB forces you to think about consistency at the operation level, not system level.
7.2 Case Study: Google Spanner#
The impossible made possible
- Strong consistency (linearizability)
- Global distribution (multiple continents)
- High availability (99.999% SLA for multi-region instances)
How?
- TrueTime API
- GPS receivers + atomic clocks in every datacenter
- Returns: [earliest, latest] time bounds
- Uncertainty typically < 7ms
- Enables: Commit-wait for global ordering
- Paxos for replication
- Each shard replicated via Paxos
- Writes go to Paxos leader
- Synchronous: a write commits once a majority of replicas acknowledge
- In multi-region configs that majority spans regions (hence the higher latency)
- Two-phase commit with Paxos
- Transactions span shards
- Each shard is Paxos group
- Coordinator also Paxos group
- Survives any single failure
Trade-off
- Write latency: pays for commit-wait plus a Paxos round trip, so writes are slower than a single-node database
- Cross-region writes: pay at least one cross-region round trip, so they are much slower again
But: TRUE global strong consistency. No "eventually consistent" surprises.
Fresh for me, a little stale for my friends
A social graph store serves a huge read load from caches in many regions over one source-of-truth database. Users must see their own writes at once; a friend's data may lag a little. Where would you route a user's reads of their own data, and how would invalidation differ between the local region and the others?
One strong answer
- Own data: route to the cache that handled the write (a leader cache in the region), so the user reads what they just wrote.
- Everyone else's data: any cache; slight staleness goes unnoticed.
- Invalidation: synchronous in the local region, where the writer reads next; asynchronous across regions, accepting lag there.
- Why: strong freshness is paid only where a user would notice, which keeps the bulk of reads on cheap, eventually consistent caches.
7.3 Case Study: Facebook TAO#
TAO (The Associations and Objects): distributed graph store for social data
Challenge
- About a billion reads/second (2013 paper)
- Eventual consistency by default (replication lag usually under a second)
- Read-after-write within a cache tier, so users see their own writes
Solution: Read-after-write within a tier: the write's changeset is applied to the follower tier that issued it
- Write path
- Write to MySQL (source of truth)
- Invalidate local cache
- Async invalidation to other caches
- Leader cache in each region
- Read path
- Reads hit a follower cache; misses go to the leader cache, then MySQL
- A user's own recent write is already in the follower tier that issued it (eventual elsewhere)
- Cache invalidation
- Local: Synchronous
- Cross-datacenter: Async via refill
- Replication lag: typically under ~1 second (not a hard bound)
Key insight: Different consistency for different access patterns. Users care about their own data being fresh. They don't notice if friend's data is 1s stale.
Chapter 8: Common Mistakes#
8.1 Mistake 1: Assuming Strong Consistency#
WRONG: Reading from replica after write
async def update_and_read(user_id: str, new_email: str):
# Write to primary
await primary.execute(
"UPDATE users SET email = $1 WHERE id = $2",
new_email, user_id
)
# Read from replica immediately
user = await replica.fetch_one(
"SELECT * FROM users WHERE id = $1",
user_id
)
# BUG: user.email might still be the old value!
# Replication hasn't happened yet
return userCORRECT: Read from primary for read-your-writes
async def update_and_read(user_id: str, new_email: str):
# Write to primary
await primary.execute(
"UPDATE users SET email = $1 WHERE id = $2",
new_email, user_id
)
# Read from PRIMARY for read-your-writes guarantee
user = await primary.fetch_one(
"SELECT * FROM users WHERE id = $1",
user_id
)
return user8.2 Mistake 2: Ignoring Monotonic Reads#
WRONG: Reading from random replicas
async def poll_for_updates(key: str):
"""Poll for updates - broken implementation."""
while True:
# Each read might hit a different replica
value = await random_replica().read(key)
# BUG: Value might go backwards!
# Replica A has version 5
# Replica B has version 3
# User sees: 5, 3, 5, 4, 5, 3...
yield value
await asyncio.sleep(1)CORRECT: Track and enforce monotonic reads
async def poll_for_updates(key: str):
"""Poll for updates with monotonic guarantee."""
min_version = 0
while True:
# Find replica with at least our version
for replica in replicas:
result = await replica.read_versioned(key)
if result.version >= min_version:
min_version = result.version
yield result.value
break
await asyncio.sleep(1)8.3 Mistake 3: Using Strong Consistency Everywhere#
WRONG: Strong consistency for everything
async def get_product_page(product_id: str):
# ALL reads from primary - unnecessary!
product = await primary.fetch("SELECT * FROM products WHERE id = $1", product_id)
reviews = await primary.fetch("SELECT * FROM reviews WHERE product_id = $1", product_id)
inventory = await primary.fetch("SELECT quantity FROM inventory WHERE product_id = $1", product_id)
# This doesn't scale - primary is bottleneck
return render_page(product, reviews, inventory)CORRECT: Appropriate consistency per operation
async def get_product_page(product_id: str):
# Product details: Eventual (from cache/replica)
product = await cache_or_replica("products", product_id)
# Reviews: Eventual (stale reviews are fine)
reviews = await cache_or_replica("reviews", product_id)
# Inventory for DISPLAY: Eventual with short TTL
inventory = await cache.get(f"inventory:{product_id}", ttl=30)
return render_page(product, reviews, inventory)
async def reserve_inventory(product_id: str, quantity: int):
# Inventory for RESERVATION: Strong consistency
async with primary.transaction(isolation="SERIALIZABLE"):
# This is where strong consistency matters
...| Value | |
|---|---|
| Product page views at peak (assumed) | 2,000 a second |
| Reads per page (product, reviews, inventory, as in the code above) | 3 → 6,000 reads a second |
| Mistake 3: every read on the primary | 6,000 reads a second on one machine |
| Distinct products viewed in any 30-second window (assumed) | 20,000 |
| Inventory display cached with a 30 s TTL | at most 20,000 ÷ 30 ≈ ~670 misses a second, served by replicas |
| Reservations at checkout (assumed 1% of page views) | ~20 a second, on the primary with SERIALIZABLE |
Conclusion: choosing consistency per operation takes the primary from 6,000 reads a second down to about 20 locked reservations a second, at the price of a displayed count up to 30 seconds stale. That's acceptable because the reservation, not the display, is what prevents overselling.
8.4 Mistake Checklist#
- Don't assume replication is instant — Always consider replication lag
- Don't read from replica after write — Use read-your-writes pattern
- Don't use random replica selection — May break monotonic reads
- Don't use strong consistency everywhere — It doesn't scale
- Don't forget about sessions — Users have expectations about their data
- Do choose consistency per operation — Different data has different needs
Real-world consistency and common mistakes
Q1/3 Your API updates a user's email on the primary and immediately reads the user back from a replica to build the response. Users intermittently see their old email. What is the fix?
Answer & explanation
Answer: D
The replica may not have received the write yet. Reading from the primary right after your own write gives read-your-writes. A sleep only guesses at replication lag, and isolation level on the primary does nothing about a lagging replica.
Q2/3 You use DynamoDB for both the product catalog and inventory reservation. Which read setup matches the lesson?
Answer & explanation
Answer: B
DynamoDB makes you pick consistency per read: catalog data tolerates staleness and gets cheaper, faster reads, while inventory must see the latest value. Strong reads everywhere cost more and lose availability during leader election for no benefit on catalog pages.
Q3/3 What does Google Spanner pay to offer strong consistency across continents?
Answer & explanation
Answer: C
TrueTime bounds clock uncertainty, and commits wait out that uncertainty while Paxos replicates the write, so writes are slower but globally ordered. Spanner still targets very high availability and offers no "eventually consistent" surprises, so stale-read options are the wrong mental model.
Part IV: Interview Preparation#
Chapter 9: Interview Tips#
9.1 Key Phrases#
Introducing consistency requirements
"Before I design this, I need to understand the consistency requirements. For the product catalog browsing, eventual consistency is fine - we can serve from cache. But for inventory checks at checkout, we need strong consistency to prevent overselling."
Explaining the trade-off
"There's a fundamental trade-off here. Strong consistency means coordinating across replicas, which adds latency and reduces availability during partitions. Eventual consistency is faster and more available, but clients may see stale data. For this use case, I'd choose [X] because [reason]."
Discussing CAP
"CAP theorem tells us that during a network partition, we must choose between consistency and availability. For a banking system, I'd choose consistency - it's better to reject a transaction than process it incorrectly. For a shopping cart, I'd choose availability - a slightly stale cart is better than an error page."
Session consistency
"Users have a mental model that their actions are immediately visible to them. If I update my profile and refresh, I expect to see the update. This is read-your-writes consistency - not full strong consistency, but essential for user experience."
9.2 Common Questions#
| Question | Good Answer |
|---|---|
| "What consistency model would you use for a social media feed?" | "Causal consistency. Comments must appear after their parent posts, but concurrent posts from different users can appear in any order. This preserves the user experience without requiring strong consistency." |
| "How do you handle inventory for an e-commerce site?" | "Two different consistency levels. For display, eventual consistency from cache is fine. For reservation at checkout, strong consistency with SELECT FOR UPDATE to prevent overselling." |
| "What's the difference between eventual and strong consistency?" | "With strong consistency, once a write completes, all subsequent reads see that write. With eventual, a write may complete but readers might still see old data for some time. The 'eventually' has no upper bound." |
| "When would you choose CP over AP?" | "When incorrect data is worse than unavailability. Financial transactions, inventory that can oversell, unique constraint enforcement. These systems should reject requests rather than process them wrong." |
Chapter 10: Practice Problems#
Practice · problem 1
Problem 1: Airline Seat Selection#
Setup: Design the seat selection system for an airline booking site. Multiple users may try to select the same seat simultaneously.
Requirements:
- Show available seats on a plane map
- Allow users to select and reserve seats
- Prevent double-booking of the same seat
Questions:
- What consistency level for displaying available seats?
- What consistency level for reserving a seat?
- How would you handle the race condition when two users click the same seat?
Solution Outline
Displaying seats Eventual consistency acceptable. Cache the seat map with short TTL (10s). Slightly stale availability is OK for display.
Reserving seats Strong consistency required. Use optimistic locking:
-- Attempt reservation
UPDATE seats
SET status = 'reserved', reserved_by = $user_id, version = version + 1
WHERE flight_id = $flight_id AND seat_number = $seat
AND status = 'available' AND version = $expected_version
-- If 0 rows affected, seat was takenRace condition: Only one UPDATE succeeds (due to version check). Loser gets "seat already taken" message. Refresh the seat map to show updated availability.
Practice · problem 2
Problem 2: Collaborative Document Editing#
Setup: Design a Google Docs-like collaborative editor. Multiple users edit the same document simultaneously.
Requirements:
- Real-time updates visible to all editors
- Works offline (changes sync when online)
- No edits should be lost
Questions:
- What consistency model is appropriate?
- How do you handle conflicting edits?
- What happens when an offline user comes back online?
Solution Outline
Consistency model Causal consistency with conflict resolution. Edits that causally depend on each other (e.g., edit then delete) must be ordered. Concurrent edits are merged.
Conflicting edits Use Operational Transformation (OT) or CRDTs. These automatically merge concurrent operations without losing edits.
Offline sync:
- Track all local edits with vector clock
- On reconnect, sync local edits with server
- Server merges using OT/CRDT
- Return merged result to all clients
This is covered more in Day 4 (Conflict Resolution).
Chapter 11: Sample Interview Dialogue#
Scenario: Design a Shopping Cart#
"Design a shopping cart for an e-commerce site."
"Before I dive in, I'd like to clarify the consistency requirements. When a user adds an item to their cart, do they need to see it immediately? What happens if they're on multiple devices?"
"Yes, they should see their own changes immediately. Multiple devices should eventually sync."
"Got it. So I need read-your-writes consistency for the user's own cart, but eventual consistency across devices is acceptable. Let me think about the access patterns..."
You sketch on the whiteboard:
- Same session
- Add item to cart
- View cart immediately
- Must see the item I just added — read-your-writes
- Cross-device
- Add item on phone
- Open laptop 5 seconds later
- Eventually see the item — eventual consistency OK
- Checkout
- Need accurate cart contents
- Need current prices
- Strong consistency required
"For the data model, I'd store the cart in a primary database with read replicas. For the same session, I'd route reads to the primary after writes to guarantee read-your-writes. For cross-device, eventual consistency is fine - replicas will catch up within seconds."
"What if the user adds an item on their phone while offline?"
"Good question. For offline support, I'd store the cart locally on the device. When they come back online, I'd sync the local changes to the server. If there are conflicts - like they added the same item on both devices - I'd merge by keeping every item and adding up the concurrent increments (3 apples added on the phone plus 1 on the laptop gives 4). This merge never loses items; Day 4 builds it as a CRDT."
"What about checkout? How do you ensure consistency?"
"At checkout, I switch to strong consistency. I'd read the cart from the primary database, verify inventory for each item with SELECT FOR UPDATE to prevent race conditions, and lock prices at that moment. The cart might have been slightly stale during browsing, but at checkout I need the ground truth."
Summary#
Consistency models (strongest to weakest)
| # | Model | Guarantee | Use for |
|---|---|---|---|
| 1 | Linearizable | Single-copy behavior, real-time ordering | Transactions, locks, inventory |
| 2 | Sequential | Global order agreed by all nodes | Logs, message queues |
| 3 | Causal | Cause always before effect | Social media, collaboration |
| 4 | Read-your-writes | See your own updates | User profiles, settings |
| 5 | Monotonic reads | Never go backwards | Feeds, counters |
| 6 | Eventual | Converges eventually | Caching, analytics |
CAP theorem in practice
- Network partitions WILL happen
- During partition: Choose C or A
- Different operations can choose differently
- Strong consistency for critical operations
- Eventual consistency for performance
Implementation patterns
- Track write versions for read-your-writes
- Track read versions for monotonic reads
- Route to primary for strong consistency
- Use replicas for eventual consistency
Default approach
- Display/Browse: Eventual consistency (cache)
- User's own data: Read-your-writes
- Critical operations: Strong consistency
Part V: Advanced Patterns#
Five replicas: how many can fail?
A key is stored on N = 5 replicas. Every read must overlap the latest acknowledged write. Pick W (write acks) and R (read replies) so the store keeps accepting both reads and writes with as many replicas down as possible. How many failures does it survive? What would W = 5, R = 1 buy you, and what would it cost?
One strong answer
- Overlap needs W + R > N, so W + R ≥ 6.
- W = 3, R = 3: both reads and writes still reach their quorum with 2 replicas down (3 of 5 remain).
- W = 5, R = 1: a read needs only one replica, so reads are fast, but a write needs all five: one replica down blocks every write.
- So balanced majorities tolerate the most failures for both reads and writes.
Chapter 12: Quorum-Based Consistency#
12.1 Understanding Quorums#
In a system with N replicas:
- W = Number of nodes that must acknowledge a write
- R = Number of nodes that must respond to a read
For reads to see the latest acknowledged write: W + R > N. This ensures read and write sets overlap — at least one node has the latest value. (Overlap alone is not full linearizability: sloppy quorums, concurrent writes and partially failed writes can still produce anomalies.)
Example with N=3
| W | R | Result |
|---|---|---|
| 2 | 2 | Strong consistency (2+2 > 3) |
| 1 | 1 | Eventual consistency (1+1 < 3) |
| 3 | 1 | Read-optimized: fast reads, but every write needs all 3 nodes |
| 1 | 3 | Write-optimized: fast writes, but every read needs all 3 nodes |
With N = 3 replicas, W = 2 and R = 2 overlap: a write acknowledged by A and B and a read answered by B and C share B, so the highest version (v2) wins and read repair fixes C. With W = 1 and R = 1 (1 + 1 < 3) a read can hit a replica that still has v1 after v2 was acknowledged.
12.2 Quorum Implementation#
# Quorum-Based Consistency Implementation
from dataclasses import dataclass
from typing import List, Optional, Any, Tuple
import asyncio
import time
@dataclass
class QuorumConfig:
"""Configuration for quorum-based consistency."""
n: int # Total replicas
w: int # Write quorum
r: int # Read quorum
def is_strong(self) -> bool:
"""Check if config provides strong consistency."""
return self.w + self.r > self.n
def __post_init__(self):
if self.w > self.n or self.r > self.n:
raise ValueError("Quorum cannot exceed replica count")
@dataclass
class VersionedValue:
"""Value with version for conflict detection."""
value: Any
version: int
timestamp: float
class QuorumStore:
"""
Distributed store with quorum-based consistency.
Provides tunable consistency through W and R parameters.
"""
def __init__(self, replicas: List, config: QuorumConfig):
self.replicas = replicas
self.config = config
async def write(self, key: str, value: Any) -> bool:
"""
Write to W replicas (quorum write).
Returns True if quorum was achieved.
"""
timestamp = time.time()
# Get current max version
current = await self._read_from_quorum(key)
new_version = (current.version + 1) if current else 1
versioned = VersionedValue(
value=value,
version=new_version,
timestamp=timestamp
)
# Write to all replicas concurrently
tasks = [
self._write_to_replica(replica, key, versioned)
for replica in self.replicas
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Count successful writes
successes = sum(1 for r in results if r is True)
if successes >= self.config.w:
return True
else:
# Quorum not achieved - write failed
# Could attempt rollback here
return False
async def read(self, key: str) -> Optional[Any]:
"""
Read from R replicas (quorum read).
Returns the value with highest version.
"""
result = await self._read_from_quorum(key)
return result.value if result else None
async def _read_from_quorum(self, key: str) -> Optional[VersionedValue]:
"""Read from R replicas and return highest version."""
tasks = [
self._read_from_replica(replica, key)
for replica in self.replicas
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Filter successful reads (keep each value paired with its replica)
pairs = [
(replica, r) for replica, r in zip(self.replicas, results)
if isinstance(r, VersionedValue)
]
values = [v for _, v in pairs]
if len(values) < self.config.r:
raise Exception(f"Read quorum not achieved: {len(values)}/{self.config.r}")
# Return highest version (read repair could update stale replicas)
if not values:
return None
latest = max(values, key=lambda v: (v.version, v.timestamp))
# Optional: Read repair - update stale replicas
await self._read_repair(key, latest, pairs)
return latest
async def _read_repair(
self,
key: str,
latest: VersionedValue,
pairs: List[Tuple[Any, VersionedValue]]
):
"""Update replicas that have stale values."""
stale_replicas = []
for replica, value in pairs:
if value.version < latest.version:
stale_replicas.append(replica)
if stale_replicas:
# Repair stale replicas asynchronously
asyncio.create_task(
self._repair_replicas(key, latest, stale_replicas)
)
async def _repair_replicas(
self,
key: str,
value: VersionedValue,
replicas: List
):
"""Repair stale replicas with latest value."""
tasks = [
self._write_to_replica(replica, key, value)
for replica in replicas
]
await asyncio.gather(*tasks, return_exceptions=True)
async def _write_to_replica(
self,
replica,
key: str,
value: VersionedValue
) -> bool:
"""Write to a single replica."""
try:
await replica.set(key, value)
return True
except Exception as e:
logger.warning(f"Write to replica failed: {e}")
return False
async def _read_from_replica(
self,
replica,
key: str
) -> Optional[VersionedValue]:
"""Read from a single replica."""
try:
return await replica.get(key)
except Exception as e:
logger.warning(f"Read from replica failed: {e}")
raise
# Usage examples
async def demo_quorum_consistency():
# Strong consistency: W=2, R=2 with N=3
strong_config = QuorumConfig(n=3, w=2, r=2)
strong_store = QuorumStore(replicas, strong_config)
# Write requires 2/3 replicas
await strong_store.write("key", "value")
# Read requires 2/3 replicas - guaranteed to see latest write
value = await strong_store.read("key")
# Eventual consistency: W=1, R=1 with N=3
eventual_config = QuorumConfig(n=3, w=1, r=1)
eventual_store = QuorumStore(replicas, eventual_config)
# Fast writes and reads, but may return stale data
await eventual_store.write("key", "value")
value = await eventual_store.read("key") # Might be stale!12.3 Sloppy Quorums and Hinted Handoff#
# Sloppy Quorums for High Availability
class SloppyQuorumStore(QuorumStore):
"""
Quorum store with sloppy quorums and hinted handoff.
When a replica is down, writes go to a "hint" node instead.
When the original replica recovers, hints are handed off.
This improves availability at the cost of consistency guarantees.
"""
def __init__(self, replicas: List, config: QuorumConfig, all_nodes: List):
super().__init__(replicas, config)
self.all_nodes = all_nodes # Including non-replica nodes
self.hints: dict = {} # node_id -> list of hints
async def write_sloppy(self, key: str, value: Any) -> bool:
"""
Write with sloppy quorum.
If a replica is down, write to any available node as a hint.
"""
timestamp = time.time()
current = await self._read_from_quorum(key)
new_version = (current.version + 1) if current else 1
versioned = VersionedValue(
value=value,
version=new_version,
timestamp=timestamp
)
successes = 0
hints_stored = []
# Try preferred replicas first
for replica in self.replicas:
if await self._write_to_replica(replica, key, versioned):
successes += 1
else:
# Replica is down, find a substitute
substitute = await self._find_substitute(replica)
if substitute:
await self._store_hint(substitute, replica.id, key, versioned)
successes += 1
hints_stored.append((substitute, replica.id))
return successes >= self.config.w
async def _find_substitute(self, failed_replica) -> Optional:
"""Find an available node to store hint."""
for node in self.all_nodes:
if node.id != failed_replica.id and await node.is_healthy():
return node
return None
async def _store_hint(
self,
node,
target_replica_id: str,
key: str,
value: VersionedValue
):
"""Store a hinted value for later handoff."""
hint = {
"target": target_replica_id,
"key": key,
"value": value
}
# Store hint on the substitute node
await node.store_hint(hint)
async def handoff_hints(self, recovered_replica):
"""
Hand off hints to a recovered replica.
Called when a replica comes back online.
"""
for node in self.all_nodes:
hints = await node.get_hints_for(recovered_replica.id)
for hint in hints:
try:
await self._write_to_replica(
recovered_replica,
hint["key"],
hint["value"]
)
await node.delete_hint(hint)
except Exception as e:
logger.warning(f"Hint handoff failed: {e}")Chapter 13: Consistency in Practice — Database Isolation Levels#
13.1 SQL Isolation Levels Explained#
PostgreSQL/MySQL provide these isolation levels:
| Level | Behavior | Anomalies | Trade-off |
|---|---|---|---|
| READ UNCOMMITTED | Can see uncommitted changes from other transactions (PostgreSQL silently treats it as READ COMMITTED) | "Dirty reads" possible | Rarely used in practice · fastest, least consistent |
| READ COMMITTED (PostgreSQL default) | Only sees committed changes | No dirty reads · non-repeatable reads possible (value changes between reads) | Good balance for most applications |
| REPEATABLE READ (MySQL InnoDB default) | Snapshot established at the transaction's first read · same query returns same results within transaction | Phantom reads possible in some databases | Good for reports, analytics |
| SERIALIZABLE | Transactions appear to execute one at a time | No anomalies possible | Performance impact (locking or abort/retry) · required for financial transactions, inventory |
13.2 Choosing Isolation Levels#
# Isolation Level Selection by Use Case
class TransactionManager:
"""
Manager for database transactions with appropriate isolation.
"""
def __init__(self, db_pool):
self.db = db_pool
async def browse_products(self, category_id: str) -> List[dict]:
"""
Browsing products - READ COMMITTED is fine.
We don't need repeatable reads for browsing.
Seeing slightly different results on refresh is OK.
"""
async with self.db.transaction(isolation="READ COMMITTED"):
return await self.db.fetch(
"SELECT * FROM products WHERE category_id = $1",
category_id
)
async def generate_report(self, start_date: str, end_date: str) -> dict:
"""
Generating a report - REPEATABLE READ.
Report should be internally consistent.
Same query should return same results throughout.
"""
async with self.db.transaction(isolation="REPEATABLE READ"):
total_sales = await self.db.fetch_val(
"SELECT SUM(amount) FROM orders WHERE date BETWEEN $1 AND $2",
start_date, end_date
)
top_products = await self.db.fetch(
"""
SELECT product_id, SUM(quantity) as total
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
WHERE o.date BETWEEN $1 AND $2
GROUP BY product_id
ORDER BY total DESC
LIMIT 10
""",
start_date, end_date
)
# Both queries see the same snapshot
return {"total": total_sales, "top_products": top_products}
async def reserve_inventory(
self,
product_id: str,
quantity: int
) -> bool:
"""
Reserving inventory - SERIALIZABLE.
Must prevent race conditions that cause overselling.
"""
async with self.db.transaction(isolation="SERIALIZABLE"):
# Consistent snapshot; a conflicting concurrent transaction is aborted
# with a serialization failure (SQLSTATE 40001), so callers must retry
current = await self.db.fetch_one(
"SELECT quantity FROM inventory WHERE product_id = $1",
product_id
)
if current['quantity'] < quantity:
return False
await self.db.execute(
"UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2",
quantity, product_id
)
return True
async def transfer_funds(
self,
from_account: str,
to_account: str,
amount: float
) -> bool:
"""
Money transfer - SERIALIZABLE with explicit locks.
Double protection: isolation level + explicit locks.
"""
async with self.db.transaction(isolation="SERIALIZABLE"):
# Lock both accounts in consistent order to prevent deadlock
accounts = sorted([from_account, to_account])
balances = await self.db.fetch(
"""
SELECT account_id, balance FROM accounts
WHERE account_id = ANY($1)
ORDER BY account_id
FOR UPDATE
""",
accounts
)
balance_map = {row['account_id']: row['balance'] for row in balances}
if balance_map[from_account] < amount:
raise InsufficientFundsError()
await self.db.execute(
"UPDATE accounts SET balance = balance - $1 WHERE account_id = $2",
amount, from_account
)
await self.db.execute(
"UPDATE accounts SET balance = balance + $1 WHERE account_id = $2",
amount, to_account
)
return True13.3 Optimistic vs Pessimistic Locking#
# Optimistic vs Pessimistic Locking Comparison
class LockingPatterns:
"""
Comparison of locking strategies for consistency.
"""
# =========================================================================
# Pessimistic Locking: Lock first, then read/write
# =========================================================================
async def pessimistic_update(self, product_id: str, new_price: float):
"""
Pessimistic locking with SELECT FOR UPDATE.
Use when:
- High contention expected
- Short transaction duration
- Can tolerate lock waiting
"""
async with self.db.transaction():
# Lock the row immediately
product = await self.db.fetch_one(
"""
SELECT * FROM products
WHERE id = $1
FOR UPDATE -- Acquires exclusive lock
""",
product_id
)
if not product:
raise NotFoundError()
# Other transactions wait here until we commit
await self.db.execute(
"UPDATE products SET price = $1 WHERE id = $2",
new_price, product_id
)
async def pessimistic_with_nowait(self, product_id: str, new_price: float):
"""
Pessimistic locking with NOWAIT - fail fast if locked.
Use when:
- Can't afford to wait
- Will retry or fail gracefully
"""
async with self.db.transaction():
try:
product = await self.db.fetch_one(
"""
SELECT * FROM products
WHERE id = $1
FOR UPDATE NOWAIT -- Fails immediately if locked
""",
product_id
)
except LockNotAvailableError:
raise ConflictError("Product is being modified")
await self.db.execute(
"UPDATE products SET price = $1 WHERE id = $2",
new_price, product_id
)
# =========================================================================
# Optimistic Locking: Read version, check at write time
# =========================================================================
async def optimistic_update(
self,
product_id: str,
new_price: float,
expected_version: int
):
"""
Optimistic locking with version check.
Use when:
- Low contention expected
- Long-running operations (user editing form)
- Can retry on conflict
"""
result = await self.db.execute(
"""
UPDATE products
SET price = $1, version = version + 1
WHERE id = $2 AND version = $3
""",
new_price, product_id, expected_version
)
if result.rowcount == 0:
# Either not found or version mismatch
current = await self.db.fetch_one(
"SELECT version FROM products WHERE id = $1",
product_id
)
if current is None:
raise NotFoundError()
else:
raise OptimisticLockError(
f"Version mismatch: expected {expected_version}, "
f"got {current['version']}"
)
async def optimistic_with_retry(
self,
product_id: str,
update_fn: Callable,
max_retries: int = 3
):
"""
Optimistic locking with automatic retry.
Retries on conflict with exponential backoff.
"""
for attempt in range(max_retries):
# Read current state
product = await self.db.fetch_one(
"SELECT * FROM products WHERE id = $1",
product_id
)
if not product:
raise NotFoundError()
# Apply update function
updated = update_fn(product)
# Try to save with version check
result = await self.db.execute(
"""
UPDATE products
SET price = $1, name = $2, version = version + 1
WHERE id = $3 AND version = $4
""",
updated['price'], updated['name'],
product_id, product['version']
)
if result.rowcount > 0:
return updated # Success!
# Conflict - retry with backoff
await asyncio.sleep(0.1 * (2 ** attempt))
raise OptimisticLockError(f"Failed after {max_retries} retries")Quorums, isolation and locking
Q1/3 A store has N=5 replicas. Which configuration guarantees every read quorum overlaps the most recent write quorum?
Answer & explanation
Answer: C
Overlap requires W + R > N. Only 3 + 3 = 6 exceeds 5; the other options sum to 5 or less. W=2, R=3 feels like a majority but 2 + 3 = 5 is not greater than N, so a read can miss the write entirely.
Q2/3 Why does a sloppy quorum with hinted handoff weaken the W + R > N guarantee?
Answer & explanation
Answer: A
The overlap argument assumes both quorums come from the same N replicas. Hints sit on other nodes until handoff, so a read from the real replicas can be stale. That is the trade: higher write availability when replicas are down, weaker consistency until hints are handed off.
Q3/3 An admin edits a product form for several minutes before saving, and two admins rarely edit the same product. Which locking strategy fits?
Answer & explanation
Answer: D
Low contention plus long think time is the textbook case for optimistic locking; you pay only when a rare conflict actually happens. Holding a pessimistic row lock for minutes blocks every other writer for the whole edit session.
Further Reading#
Papers#
- Book
"Linearizability: A Correctness Condition for Concurrent Objects" by Herlihy & Wing
- Book
"Causal Memory: Definitions, Implementation, and Programming" by Ahamad et al.
- Book
"Eventually Consistent" by Werner Vogels (Amazon CTO)
Books#
- Book
"Designing Data-Intensive Applications" by Martin Kleppmann — Chapters 5, 9
- Book
"Database Internals" by Alex Petrov — Consistency chapters
Engineering Blogs#
- Docs
AWS DynamoDB Consistency: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html
- Paper
Google Spanner Paper: https://research.google/pubs/pub39966/
this lesson in shorts · 2
play them all →One idea from this lesson in about a minute each, made for your phone.
Go deeper · reading trail
all reading →Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.
Eventually Consistent - Revisited
Werner Vogels · 2008- What
- Amazon's CTO on what eventual consistency means for the clients of a replicated store.
- Key idea
- Client-side guarantees such as read-your-writes, session and monotonic reads can be offered on top of eventual consistency, and N, W and R quorums set the trade-off.
- Why here
- The consistency spectrum and quorum chapters of this lesson, explained by the person behind Dynamo.
Consistency Models
Kyle Kingsbury- What
- Jepsen's clickable map of consistency models and how they relate.
- Key idea
- Models form a hierarchy, from strict serializable and linearizable down to causal, read-your-writes and eventual, each with what it forbids.
- Why here
- A reference for the consistency spectrum of this lesson, with each model's definition one click away.
Replicated Data Consistency Explained Through Baseball
Doug Terry · 2011- What
- A short paper that explains six consistency guarantees using the people who follow a baseball game.
- Key idea
- The scorekeeper, umpire, radio reporter and others each need a different guarantee (strong, bounded staleness, consistent prefix, monotonic reads, read-my-writes, eventual).
- Why here
- The best way into this lesson's main point: choose consistency per reader and per operation, not per database.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

