Design a Distributed Key-Value Store

Harddistributed systems · consistent hashing · replication · quorum · cap theorem · lsm tree⏱ 8 min readAsked at Amazon, Google, Microsoft, Meta, UberUpdated

Build a Dynamo-style key-value store that partitions data with consistent hashing, replicates it with tunable quorums, and stays writable through node failures.

Requirements

A key-value store exposes the simplest possible interface, put(key, value) and get(key), but doing it across hundreds of machines forces you to confront partitioning, replication, consistency and failure handling all at once. Amazon's Dynamo paper is the canonical blueprint and the basis for Cassandra, Riak and DynamoDB's ancestry.

Functional

  • put(key, value) stores or overwrites a value; get(key) returns it; delete(key) removes it.
  • Keys up to ~256 bytes, values up to ~1 MB (larger blobs belong in object storage).
  • Optional per-key TTL.
  • Per-request consistency level (for example ONE, QUORUM, ALL).

Non-functional

  • Scale: tens of terabytes and hundreds of thousands of operations per second; grows by adding nodes.
  • High availability: writes must succeed even when some replicas are down ("always writable").
  • Low latency: p99 under ~10 ms within a region.
  • Durability: an acknowledged write survives machine loss.
  • Tunable consistency: callers choose between eventual and strong-ish reads.
Tip

Ask whether the interviewer wants a CP system (like etcd or ZooKeeper: consensus, strong consistency, smaller data) or an AP system (like Dynamo or Cassandra: always writable, eventual consistency, huge data). The two designs diverge almost immediately.

Capacity Estimates

Assume 10 B keys, 1 KB average value, 300 K reads/s and 100 K writes/s at peak, replication factor 3.

Metric Math Value
Raw data 10 B × 1 KB ~10 TB
With 3 replicas 10 TB × 3 ~30 TB
Storage nodes (2 TB usable each, 50% headroom) 30 TB / 1 TB ~30 nodes
Replica write ops 100 K × 3 300 K/s cluster-wide
Replica read ops (quorum R=2) 300 K × 2 600 K/s cluster-wide
Ops per node 900 K / 30 ~30 K/s per node
Network in (writes) 300 K × 1 KB ~300 MB/s cluster-wide

Roughly 30 commodity nodes handle the load, so per-node performance is fine; the hard part is keeping 30 nodes (and later 300) correct while they fail independently.

API Design

text
PUT /v1/kv/{key}?consistency=QUORUM&ttl=3600
  Body:    <opaque bytes>
  Header:  If-Match: <version-context>      (optional, for read-modify-write)
  200:     { "version": "<context>" }

GET /v1/kv/{key}?consistency=ONE|QUORUM|ALL
  200:     { "value": "<base64>", "version": "<context>" }
  300:     { "siblings": [ ... ] }            (conflicting versions the client must merge)
  404:     not found

DELETE /v1/kv/{key}?consistency=QUORUM      (writes a tombstone)

The version context (a vector clock or version number) is opaque to the client: it reads it on get and passes it back on the next put so the store knows which version the write supersedes.

Data Model

Every node stores rows of the form:

Field Type Notes
key bytes Hashed to choose the partition
value bytes Opaque to the store
version vector clock or (timestamp, node) Conflict detection or last-write-wins
tombstone boolean Deletes are writes, so they replicate too
expires_at timestamp TTL, enforced lazily on read and during compaction

Cluster metadata (which node owns which ring range, node status) is tiny and is spread by gossip or held in a small consensus store.

High-Level Design

flowchart LR
  Client --> LB[Load Balancer / smart client]
  LB --> C1[Coordinator node]
  C1 --> R1[(Replica A)]
  C1 --> R2[(Replica B)]
  C1 --> R3[(Replica C)]
  subgraph Node["Inside each storage node"]
    WAL[Commit log] --> MT[Memtable]
    MT -- "flush" --> SST[(SSTables + Bloom filters)]
  end
  R1 -. "gossip, hinted handoff, Merkle-tree repair" .- R2
  R2 -. "gossip" .- R3

There is no master. Any node can act as the coordinator for a request:

  1. The coordinator hashes the key, finds its position on the consistent-hash ring and picks the first N healthy owners (the preference list).
  2. Write: send to all N replicas, acknowledge the client after W replicas confirm.
  3. Read: ask the replicas, return once R respond, resolving any version differences.
  4. Background processes (gossip, hinted handoff, anti-entropy) repair whatever the fast path missed.

A "smart client" library that knows the ring can send requests straight to a replica and skip one hop.

Deep Dives

Partitioning with consistent hashing

Hashing keys with hash(key) % N reshuffles almost every key when N changes. Instead, place nodes and keys on a hash ring; each key belongs to the next node clockwise. Adding a node moves only the keys in one arc.

  • Give each physical node many virtual nodes (tokens) so load is even and, when a node leaves, its ranges are spread across many survivors rather than dumped on one neighbour.
  • More powerful machines get more virtual nodes, which handles heterogeneous hardware.
  • See the Consistent Hashing article for the ring itself.

Replication and quorums

Each key is stored on N nodes: its owner plus the next N-1 distinct physical nodes clockwise (skipping virtual nodes of the same machine, and ideally spanning racks or availability zones).

With N replicas, a write waits for W acknowledgements and a read for R responses:

Setting (N=3) Behaviour
W=3, R=1 Fast reads, writes fail if any replica is down
W=1, R=3 Fast writes, slow reads
W=2, R=2 Balanced; R + W > N so every read overlaps the latest write
W=1, R=1 Lowest latency, eventual consistency only
Insight

R + W > N guarantees the read set and write set share at least one replica, so the read sees the newest acknowledged value. It is not full linearizability: concurrent writes, sloppy quorums and partial failures can still expose stale or conflicting data. Strict linearizability needs consensus (Raft, Paxos).

Handling failures: sloppy quorum and hinted handoff

If a replica in the preference list is unreachable, a strict quorum would reject the write. Dynamo instead uses a sloppy quorum: the coordinator writes to the next healthy node on the ring, tagging the data with a hint naming the intended owner. When the owner recovers, the stand-in hands the data back and deletes its copy.

  • Failure detection: nodes gossip heartbeats every second; a node not heard from for a while is marked suspect locally. No central authority is needed.
  • Permanent failures: anti-entropy repair compares replicas using Merkle trees per key range; only subtrees whose hashes differ are exchanged, so two mostly identical 1 TB replicas sync by sending kilobytes.
  • Read repair: when a quorum read sees a stale replica, the coordinator writes the newest version back to it.

Versioning and conflict resolution

With always-writable replicas, two clients can update the same key on different sides of a partition. The store must detect and resolve the divergence.

  • Last-write-wins (LWW): keep the value with the highest timestamp. Simple, used by Cassandra, but silently drops one of the concurrent writes and depends on clock sync.
  • Vector clocks: each version carries {node: counter} pairs. If one clock dominates the other, the older version is discarded; if neither dominates, both are kept as siblings and returned to the client to merge (Dynamo's shopping cart merged by union).
  • CRDTs: data types (counters, sets, maps) whose merge is mathematically guaranteed to converge; good when values have structure.
Warning

Deletes are writes. If you physically remove a key on one replica while another replica missed the delete, anti-entropy will "resurrect" it. Write a tombstone and garbage-collect it only after a grace period longer than the maximum repair interval.

The storage engine on each node

Write-heavy key-value stores typically use a log-structured merge (LSM) tree:

  1. Append the write to a commit log (WAL) on disk for durability.
  2. Insert into an in-memory sorted memtable.
  3. When the memtable is full, flush it as an immutable sorted SSTable file.
  4. Reads check the memtable, then SSTables from newest to oldest; a per-SSTable Bloom filter skips files that cannot contain the key.
  5. Compaction merges SSTables in the background, dropping overwritten values, expired TTLs and old tombstones.

Writes become sequential disk I/O, which is why LSM stores sustain very high write throughput. The cost is read and space amplification, which compaction strategy tuning trades off.

Scaling & Reliability

  • Adding capacity: a new node takes over token ranges and streams only those ranges from existing replicas; clients keep working throughout.
  • Rack and zone awareness: place the N replicas in different failure domains so a zone outage loses at most one copy.
  • Multi-region: replicate asynchronously between regions and offer LOCAL_QUORUM so requests never wait on cross-region latency.
  • Hot keys: a celebrity key can overwhelm its N replicas; add a cache in front, or split the value across sub-keys.
  • Backpressure: bound coordinator queues and shed load rather than letting latency explode cluster-wide.
  • Observability: track per-node p99, pending hints, compaction backlog and repair progress.

Trade-offs

Decision Option A Option B Notes
CAP stance CP (consensus, refuse writes on partition) AP (sloppy quorum, always writable) Dynamo-style stores choose AP
Consistency Strong (R + W > N, or consensus) Eventual (R = W = 1) Tunable per request
Conflict resolution Last-write-wins (simple) Vector clocks / CRDTs (no lost updates) LWW is the common default
Storage engine LSM tree (fast writes) B-tree (fast reads, in-place updates) LSM for write-heavy workloads
Membership Gossip (decentralised) Central config service (simple) Gossip scales without a SPOF
Request routing Any-node coordinator Smart client Smart client saves a hop

Interview Tips

  • Declare CP vs AP up front and justify it with the use case (shopping cart vs account balance).
  • Build the design in layers: partitioning, then replication, then consistency, then failure handling, then storage engine.
  • Explain the quorum condition R + W > N with a concrete N=3 example and be honest about its limits.
  • Distinguish temporary failures (sloppy quorum, hinted handoff) from permanent ones (Merkle-tree anti-entropy).
  • Expect a follow-up on concurrent writes; compare last-write-wins with vector clocks.
  • Mention tombstones and compaction; they show you know how deletes really work.
  • Use the capacity math to size the cluster and show per-node load is modest.

References

My notes saved in this browser