Designing a Dynamo-Style KV Store: Quorums, Hinted Handoff, and Vector Clocks
Amazon's Dynamo paper starts from a business constraint, not a technical one: the shopping cart must accept writes during any failure, because a rejected add-to-cart is lost revenue. Everything strange about Dynamo-style stores — sloppy quorums, vector clocks, conflicts handed back to the application — follows from refusing to ever say no to a write. If Spanner is what "choose consistency" looks like when fully engineered, Dynamo is the fully engineered version of choosing availability.
The skeleton
Keys placed on a consistent hash ring; each key replicated to N nodes (the next N distinct physical nodes clockwise — the "preference list"). Every node can accept any request and route it (or clients route directly with ring awareness). No leader, no failover event — the design's first big claim: no single point whose death needs detecting before writes continue.
Quorums: tuning knobs, not guarantees
Write to W replicas before acking; read from R. With N=3:
| Config | Behavior |
|---|---|
| W=3, R=1 | Slow writes, fast reads, no overlap during failures |
| W=1, R=1 | Fast everything, weakest guarantees |
| W=2, R=2 | R+W > N: read set and write set must intersect — reads see the latest acked write, usually |
"Usually," because Dynamo quorums are sloppy: if a preference-list node is unreachable, the write lands on the next healthy node around the ring instead, tagged with a hint ("this belongs to node A — deliver when it recovers"). That is hinted handoff: availability preserved, but now R+W>N is not a hard intersection guarantee — the read quorum may miss the hinted write entirely. Say this in an interview and you have separated yourself from everyone who memorized "R+W>N means strong consistency."
put(k, v): coordinator -> N preference nodes
node C down -> write to D with hint(C)
ack after W responses
get(k): read R replicas -> versions differ? -> resolve, read-repair
Versioning: what "conflict" means without a leader
Two clients write the same key through different coordinators during a partition. Neither write is "later" in any meaningful sense — wall clocks lie. Two schemes:
Last-write-wins (LWW): highest timestamp survives. Simple, and silently drops one write. Cassandra chose this default — fine for "latest sensor reading," quietly catastrophic for a shopping cart.
Vector clocks: each version carries {node: counter} per updating node. Comparing clocks tells you descends (one includes the other's history — keep the newer) or concurrent (neither does — keep both as siblings and return both on read; the application merges).
v1 {A:1} v2 {A:1,B:1} -> v2 descends v1: keep v2
v2 {A:1,B:1} v3 {A:2} -> concurrent: siblings, client merges
cart merge: union of items (deletes need tombstones, or they resurrect)
The cart merge is the famous example precisely because union is a safe merge — and the equally famous fine print is deletion: without tombstones recording "removed at version X," a merge resurrects deleted items. Deletion in leaderless systems is always a recorded event, never an absence (the same lesson as Kafka compaction tombstones). Modern descendants push merging into the data types themselves — CRDTs are vector-clock-era conflict handling made systematic.
Repair: three layers, three time scales
Replicas drift — hinted writes, missed messages, node replacement. Convergence is manufactured:
- Read repair (milliseconds): read touches R replicas; any stale ones get updated with the winning/merged version inline. Hot keys self-heal.
- Hinted handoff replay (minutes): recovered nodes receive their held hints.
- Anti-entropy (hours, continuous): background pairwise sync of entire key ranges using Merkle trees — replicas exchange hash-tree roots and descend only into differing subtrees, so comparing a billion keys costs log-depth hash exchanges plus the actual diffs. Cold keys that nobody reads still converge.
That trio — inline, event-driven, and scheduled repair — is a reusable pattern worth naming for any eventually consistent design.
Membership and failure detection
No master decides who is alive. A gossip protocol spreads membership and liveness: each node periodically exchanges state with random peers; failures are suspected locally (missed heartbeats through gossip) and acted on locally (route around, write hints). Ring changes (join/leave) propagate the same way, shifting key ranges gradually. Coordination-free membership is what makes "any node can serve any request" true even while the cluster's view is briefly inconsistent — the design tolerates a fuzzy membership view because every layer above it already tolerates staleness.
Where this design wins and loses
Wins: write availability through partitions and node loss, predictable low latency (no cross-region consensus in the hot path), smooth incremental scaling, operational symmetry (every node identical). Loses: no transactions, no "read your own write" without careful quorum/session tricks, conflict handling exported to application code, tombstone and sibling management as a permanent operational tax.
Deployed descendants each moved one dial: Cassandra kept the ring but chose LWW + tunable consistency; Riak kept vector clocks/CRDTs; DynamoDB-the-product hid all of it behind a managed API with LWW semantics and added optional strong reads. The interview move is to state the dial explicitly: per-key linearizability needs a leader or consensus somewhere — if the requirement is "never refuse a write," this is the architecture, and conflict resolution is the bill, payable by the application.
Keep reading
Designing Ad Click Aggregation: Exactly-Once Counting at Scale
Billions of clicks, billed to the cent: streaming aggregation with watermarks, dedupe, idempotent sinks, and lambda-style reconciliation.
Designing a CDN: Cache Hierarchy, Invalidation, and Request Routing
How a CDN actually works: edge PoPs, origin shields, consistent-hash cache keys, purge fan-out, and the anycast vs DNS routing decision.
Designing a Distributed Cache Service: Redis Cluster Internals and Hot Keys
Build the cache, not just use it: slot-based sharding, gossip and failover, eviction under memory pressure, and the hot-key problem that shards can't solve.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.