system-design

Design a Distributed Key-Value Store (like DynamoDB / Cassandra)

Difficulty: Tier 3 (deep) Asked at: Amazon, Google, Meta, senior/staff loops Time budget: 45–60 min

This is the question that separates people who use distributed systems from people who understand them. You’re building the storage layer everything else in this repo depends on — so every hard concept shows up at once: partitioning, replication, consistency, quorums, conflict resolution, failure detection. It’s a synthesis exam. Don’t attempt it until you’re comfortable with Part 4.

Prerequisites: Consistent Hashing, Replication, Quorums, Conflict Resolution / CRDTs, CAP


1. Requirements

Functional:

Non-functional:

Out of scope: transactions across keys, SQL queries, secondary indexes (mention they’re hard here).


2. Estimation


3. API

put(key, value, [context])   → success / version
get(key)                     → value(s) + version context
delete(key)                  → tombstone

Note get may return multiple versions if there was a conflict (see §6c) — the client (or a merge function) resolves them.


4. The core decisions

This design is a set of interlocking choices. Present them as a system, not a list.

4a. Partitioning — where does a key live?

Consistent hashing (chapter). Hash the key onto a ring; the key belongs to the next node clockwise. Virtual nodes (each physical node owns many ring positions) keep the load even and make adding/removing nodes cheap — only a small slice of keys moves. 🚨 This is the reason to use consistent hashing over hash % N: elasticity without a full reshuffle.

4b. Replication — how many copies?

Each key is stored on the N nodes following its position on the ring (the “preference list”). N = 3 is typical. Replicas span racks/availability zones so a rack failure doesn’t lose all copies. Writes go to all N; reads can consult a subset.

4c. Consistency — quorums (the knob)

🚨 The central mechanism. With replication factor N, require:

If R + W > N, reads and writes overlap on at least one node → you read the latest write (strong-ish consistency). Tune the knob:

This is CAP made tunable per-operation. (Quorums)


5. High-level design

flowchart TB
    Client --> Coord[Coordinator node<br/>any node can coordinate]
    Coord -->|"put to N replicas"| R1[Replica 1]
    Coord --> R2[Replica 2]
    Coord --> R3[Replica 3]
    Ring[Consistent hash ring<br/>+ virtual nodes] -.routes.-> Coord
    Gossip[Gossip protocol<br/>membership & failure detection] -.-> Coord

Decentralized, peer-to-peer (Dynamo/Cassandra style): every node is equal, any node can coordinate a request, routing it to the N replicas for the key. No single master → no single point of failure. Nodes learn about each other and detect failures via gossip.


6. Deep dives

6a. Writes with hinted handoff

A write goes to the N preference-list nodes. If one is temporarily down, a different healthy node accepts the write on its behalf with a hint (“this really belongs to node X”), and hands it off when X recovers. 🚨 Hinted handoff = you can always accept writes (high availability) even when a replica is down — a key Dynamo idea.

6b. Reads, read-repair, and anti-entropy

A read queries R replicas. If they disagree (some stale), return the newest and repair the stale ones in the background (read-repair). For replicas that stayed divergent (missed writes), a background anti-entropy process using Merkle trees efficiently finds and reconciles differences without comparing every key. (Replication)

6c. Conflict resolution — the hard part

With high availability and no single master, two clients can write the same key concurrently on different replicas → conflicting versions. How to reconcile?

6d. Membership & failure detection

Nodes gossip their view of the cluster — who’s up, who’s down, ring positions — so membership converges without a central registry. Failure detection is done via heartbeats/gossip with a suspicion mechanism (a node is suspected before declared dead to avoid flapping). (Gossip)

6e. Storage engine on each node

Each node stores its keys locally in an LSM-tree (memtable + SSTables + compaction) — optimized for high write throughput, which a KV store needs. (B-tree vs LSM)


7. Bottlenecks & scaling further

  1. Adding capacity → add nodes; virtual nodes rebalance only a slice of keys, online.
  2. Hot key → even consistent hashing puts a single hot key on N nodes; extreme hotspots need caching in front or key-splitting. (Hot Keys)
  3. Wide rows / large values → chunk them; keep values bounded.
  4. Cross-key transactions → not natively supported; push to the application or a layer above (this is a deliberate limitation you should name).

8. Trade-off summary

Decision Chosen Alternative Why
Architecture Peer-to-peer, no master Leader-based No single point of failure; always writable
Partitioning Consistent hashing + vnodes hash % N Elastic — add nodes without full reshuffle
Consistency Tunable quorum (R+W>N) Fixed strong / eventual Caller picks the C/A/latency trade per op
Availability Hinted handoff Reject writes on failure Accept writes even when replicas are down
Conflicts Vector clocks / LWW / CRDT Ignore (last write) Reconcile concurrent writes explicitly
Storage engine LSM-tree B-tree Write-optimized

9. Follow-up questions

Explain R + W > N in one breath. If the number of replicas a write must reach (W) plus the number a read must reach (R) exceeds the total replicas (N), then the read set and write set must overlap on at least one node — so any read is guaranteed to see at least one replica that has the latest acknowledged write, giving you strong-ish consistency. If R + W ≤ N they might not overlap, so a read can miss the newest write → eventual consistency. Tuning R, W, and N is how you slide between consistency, latency, and availability per operation.
Why consistent hashing instead of hash(key) % N? With `% N`, changing N (adding/removing a node) changes the modulus, so *almost every key* remaps to a different node — a massive reshuffle that's catastrophic at petabyte scale. Consistent hashing places nodes and keys on a ring so adding/removing a node only moves the keys between that node and its neighbor — a small fraction. Virtual nodes further even out the load and smooth the rebalance. Elasticity without mass data movement is the whole point.
What does hinted handoff buy you? Availability for writes during transient failures. If a replica in the preference list is temporarily unreachable, instead of failing the write (or blocking), another healthy node accepts it and stores a hint noting the intended owner; when that owner recovers, the hint is replayed to it. So the system keeps accepting writes at full W even with a node down, then heals — you get durability and availability without waiting for the failed node.
How do you detect and reconcile replicas that drifted apart? Two mechanisms. Read-repair fixes divergence lazily: on a read, if replicas disagree, the coordinator returns the newest version and writes it back to the stale replicas. Anti-entropy fixes it proactively in the background using Merkle trees — each replica builds a hash tree over its key ranges, and two replicas compare trees top-down, descending only into subtrees whose hashes differ, so they find the exact keys that diverged without scanning everything. Together they bound how long replicas can stay inconsistent.
When would you NOT choose an AP key-value store like this? When you need strong consistency and multi-key transactions — e.g. a bank ledger where you must never read stale balances or lose a write, or anything requiring ACID across records. Then you want a CP system (single-leader with consensus like a relational DB, Spanner, or a Raft-backed store) that sacrifices some availability during partitions for correctness. This KV store optimizes for availability and scale and deliberately gives up transactions and strong consistency by default — the right tool only when those trade-offs fit.

10. What junior / mid / senior answers look like


Further reading