Design a Key-Value Store
Design a distributed, highly available key-value store: consistent hashing, replication with tunable quorums, conflicts, repair and the storage engine.
On this page
A key-value store does two things: put(key, value) and get(key). On one machine that is a hash map with a log. Spread it over thirty machines that fail, and you must decide where each key lives, how many copies to keep, what a read returns while copies disagree, and how they heal.
Interviewers ask it to test whether you are precise about guarantees; "it's eventually consistent" is not an answer. It follows A Framework for Any Design and leans on Consistent Hashing and Replication.
Clarify the problem
The design swings on one answer: during a failure, do you refuse writes or accept them and sort out conflicts later? Ask it early.
Design a distributed key-value store.
What are keys and values like, and do we need anything beyond get, put and delete, like range scans or transactions?
Keys up to 256 bytes, values up to 1 MB but usually around 10 KB. Just get, put and delete.
How much data and traffic?
About 2 billion keys. At peak, 150,000 reads and 50,000 writes a second.
If a node or a network link fails, would you rather reject writes, or accept them and resolve conflicts afterwards? Think of a shopping cart.
Accept them. Losing an "add to cart" costs us money. Some callers want stronger reads, though.
Then I'll favour availability, make consistency tunable per request, and never lose a write once it's acknowledged. One region with three zones; multi-region as a follow-up.
- Functional:
get,putanddeleteby key; the caller chooses how many replicas must answer. - Non-functional: writes are accepted even when some replicas of a key are down; p99 under 10 ms within the region; at the default write level, an acknowledged write survives any single node failure; the cluster grows by adding nodes without downtime.
- Out of scope: range scans, secondary indexes, multi-key transactions, multiple regions.
Estimate the scale
- Raw data: 2 billion keys × 10 KB = 2 × 1013 B = 20 TB.
- Write bandwidth: 50,000 × 10 KB = 500 MB/s from clients; with 3 copies, 1.5 GB/s of replica writes across the cluster.
- Read bandwidth: 150,000 × 10 KB = 1.5 GB/s back to clients.
How much disk do the replicated values need in total?
- 2 billion keys, about 10 KB each
- 3 copies of every key
- Ignore index and compaction overhead
Show the working
- Raw: 2 × 109 × 104 B = 2 × 1013 B = 20 TB
- Replicated: 20 TB × 3 = 60 TB
Estimate 60 TB (anything from 20 TB to 200 TB is a good answer)
Keep each 4 TB node half full, leaving room for compaction and growth: 60 ÷ 2 = 30 nodes.
With N = 3, writes to all 3 replicas, reads from 2, and 30 nodes, about how many replica operations does each node handle at peak?
- 150,000 reads and 50,000 writes a second at peak
- Each write goes to 3 replicas; each read asks 2
- 30 nodes, load spread evenly
Show the working
- Replica reads: 150,000 × 2 = 300,000 a second
- Replica writes: 50,000 × 3 = 150,000 a second
- (300,000 + 150,000) ÷ 30 = 15,000 a second per node
Estimate 15 thousand/s (anything from 5 thousand/s to 50 thousand/s is a good answer)
Comfortable for one node with SSDs. The node count is set by disk, not by traffic.
Write the three operations. What does get return when replicas hold two different values, and what must the caller send back on put?
Compare with a model answer
A get can return several values (siblings) plus an opaque context holding their versions. The next put sends it back, so the store knows which versions the new value replaces.
Define the API
get(key, read = "quorum")
-> { values: [v1, ...], context } one value normally; several after a conflict
-> NOT_FOUND
-> UNAVAILABLE fewer than R replicas answered in time
put(key, value, context?, write = "quorum")
-> OK W replicas have it in their commit logs
-> UNAVAILABLE fewer than W confirmed in time (it may
still be stored on some replicas)
-> TOO_LARGE key over 256 B or value over 1 MB
delete(key, context?, write = "quorum") writes a tombstone
read / write levels: "one" | "quorum" | "all" (R or W out of N = 3)The decision worth arguing is the context. Without it, put overwrites and the newest timestamp wins, which silently drops one of two concurrent writes. With it, the store can tell "this replaces what you read" from "these happened in parallel" and hand both back to merge. It costs callers complexity, so make it optional: no context means last-write-wins.
Note the honest UNAVAILABLE on put: a write that reached 1 of 2 required replicas reports failure but may still be read later. Callers treat it as "unknown" and retry.
What does one stored record hold besides the key and value? And what does each node need to know about the cluster?
Compare with a model answer
Each record carries a version vector, a timestamp and a tombstone flag. Each node also holds the ring and which nodes are alive, learned by gossip.
Model the data
record (on disk, sorted by key)
key up to 256 B
value up to 1 MB, about 10 KB on average
version version vector, e.g. {B: 3, C: 1}, about 16 B per entry
timestamp 8 B, used only for last-write-wins and tombstone expiry
tombstone 1 bit: this key was deleted
cluster metadata (same on every node, a few MB)
ring node -> its virtual-node positions on the hash ring
membership node -> zone, status (up / suspected / leaving), heartbeatEvery operation is a single-key lookup or overwrite. Writes are a quarter of the traffic, but that is 5,000 replica writes a second per node, every one to a random key. An LSM tree turns them into sequential appends; Indexing explains how. Its cost is reads that may check several files, which Bloom filters keep cheap. A B-tree would be the pick if writes were rare.
Sketch a put: who receives it, how it finds the nodes that own the key, and when it can say "done". Then sketch what happens when one of those nodes is down.
Compare with a model answer
No leader. The node that receives the request hashes the key, walks the ring to the next 3 distinct nodes, sends the write to all 3 and replies after 2 confirm. If an owner is down, a stand-in keeps the write as a hint and passes it on later.
High-level design
There is no leader. Any node can coordinate any request, and every node knows the ring. Keys sit on a hash ring with about 100 virtual nodes per server. A key's preference list is the first 3 distinct servers clockwise from its hash, chosen so the 3 copies land in 3 different zones.
request and replyasynchronous message
- N
- 3
- W
- 2
- acks
- 0
1The client calls
put("cart:42", [milk]). Whichever node receives it becomes the coordinator.- N
- 3
- W
- 2
- acks
- 0
2The coordinator hashes the key and walks clockwise on the ring: the first 3 distinct servers are B, C and D, one per zone. Every node has the ring, so no lookup.
- N
- 3
- W
- 2
- acks
- 0
3It sends the write to all 3 in parallel but waits for only 2: every copy stays current, and latency is set by the second-fastest.
- N
- 3
- W
- 2
- acks
- 2
4Each replica appends the write to its commit log on disk, then updates its in-memory table. Only then does it confirm. B and C confirm first.
- N
- 3
- W
- 2
- acks
- 3
52 confirmations = W, so the client hears OK. The write is on disk in two zones, at version
{B: 1}: B, the key's first owner, stamped it. D confirms a moment later; nobody waited.- N
- 3
- R
- 2
- answers
- 2
6A read,
get("cart:42")with R = 2. The coordinator asks the replicas and uses the first 2 answers: B and D, both at version{B: 1}. They agree, so it returns [milk].- N
- 3
- W
- 2
- acks
- 0
7Now D stops answering, rebooting or cut off. A new write arrives for the same key.
- N
- 3
- W
- 2
- acks
- 2
8B and C get the write as usual. Instead of D, the coordinator sends a copy to E, the next healthy node on the ring, tagged "this belongs to D". That is a hinted handoff.
- N
- 3
- W
- 2
- acks
- 3
9B and C confirm, which meets W = 2 with real owners. Had only one owner been up, E's copy would count toward W instead: a sloppy quorum that stays available but weakens the read guarantee.
10D comes back 10 minutes later. E sees it in gossip, sends it the hinted writes and deletes them. No full repair needed.
Deep dives
Partitioning with consistent hashing
Each server gets about 100 points on the ring (virtual nodes); Consistent Hashing covers why. What a replicated store adds:
- Skip duplicates when walking the ring. Neighbouring points may belong to one server, so take the next 3 distinct servers in 3 distinct zones. Otherwise one zone outage can take every copy of a key.
- Rebuilds are parallel. A dead node's 2 TB is spread in small ranges over the other 29. If each streams its share at 100 MB/s, the rebuild takes 2 × 1012 ÷ (29 × 108) ≈ 690 s, about 11 minutes. From one peer it would take 5.6 hours.
Replication and tunable quorums
Each key has N = 3 replicas. A write waits for W of them, a read for R. Replication traces why R + W > N makes read and write sets overlap. In the interview, pick values per use case:
| Setting (N = 3) | What you get | What it costs |
|---|---|---|
| W = 2, R = 2 | Reads see the latest acknowledged write in the normal case; survives one replica down for both. | Each op waits for the second-fastest replica. The default. |
| W = 1, R = 1 | Fastest; writes succeed with two replicas down. | A read can miss a write that just succeeded. Fine for caches and session data. |
| W = 3, R = 1 | Fast reads that see every acknowledged write. | Any one replica down blocks all writes to its keys. |
Be precise about the limits. R + W > N guarantees overlap, not linearizability: concurrent writes both succeed, a failed write can still appear on one replica, and two reads during a write can see new then old. A sloppy quorum breaks even the overlap: if stand-ins like E met W, a read from the real owners can miss the write until the hint arrives. Say: "Quorum reads see the latest acknowledged write while the home replicas are reachable; during failures we choose availability and accept stale reads." More in CAP and Consistency.
Resolving conflicts
Because the store takes writes during failures, two clients can update one key without seeing each other.
- B holds
- milk
- C holds
- milk
1Both replicas hold the cart [milk] at version
{B: 1}: one write, coordinated by B.- B holds
- milk, eggs
- C holds
- milk
2The link between B and C fails. The phone reads [milk] with context
{B: 1}and adds eggs. B coordinates, so it bumps its own entry:{B: 2}.- B holds
- milk, eggs
- C holds
- milk, bread
3The laptop also read [milk] at
{B: 1}. It adds bread through C, which adds its own entry:{B: 1, C: 1}.- siblings
- [milk, eggs] and [milk, bread]
4The link heals and the versions meet.
{B: 2}has the bigger B count;{B: 1, C: 1}has the bigger C count. Neither contains the other, so the writes were concurrent. Keep both as siblings.- merged
- milk, eggs, bread
5The next read returns both carts and the merged context
{B: 2, C: 1}. The cart code knows what a cart is, so it merges by union: [milk, eggs, bread].- B holds
- milk, eggs, bread
- C holds
- milk, eggs, bread
6It writes the merged cart with that context. B makes it
{B: 3, C: 1}, which covers both siblings, so both are replaced. Nothing was lost.- B holds
- milk, bread
- C holds
- milk, bread
7Contrast last-write-wins: the store keeps whichever write has the later timestamp, say the laptop's, and the eggs vanish without any error. If the laptop's clock ran fast, it wins even if it wrote first.
A version vector detects concurrency; it does not resolve it. Someone still merges, and a plain union has a flaw: an item removed on one device comes back from the other.
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Last-write-wins | Simple. One value per key, no work for callers. | Silently drops concurrent writes, and clock skew decides which one. | Values are written once, or one writer owns each key, or losing an update is harmless (a cache). |
| Version vectors + siblings Usual default | Never loses a write. The application merges with real knowledge of the data. | Every caller must handle siblings. Vectors grow with the number of coordinators. | Concurrent updates matter and callers can merge: carts, user settings. My default here. |
| Merge-friendly data types (CRDTs) | The store merges automatically and correctly: counters, sets, maps. | Only for those data shapes; removals need extra metadata. | The value is a counter or a set and you want merges without client code. |
What would change my mind: if nearly every key has one writer, last-write-wins is simpler and safe enough.
Detecting failures and repairing data
No central monitor decides who is dead. Every second, each node bumps its heartbeat counter and sends its membership table to a few random peers (gossip). If a node's counter has not moved for, say, 10 seconds, peers mark it suspected. That is an opinion: a slow node and a dead one look the same.
Treat temporary and permanent failures differently. A suspected node gets hinted handoff and no data moves; streaming 2 TB for a node that reboots in a minute does harm. Only when an operator, or automation with a long timeout, declares it gone do its ranges rebuild.
Three mechanisms repair data:
- Hinted handoff covers short outages. A node down for 10 minutes misses 5,000 writes a second × 600 s = 3 million writes, about 30 GB at 10 KB each, held by the stand-ins.
- Read repair fixes stale copies a read notices.
- Anti-entropy catches everything else, including keys nobody reads. Two replicas compare a Merkle tree, a tree of hashes over sub-ranges, and descend only into branches whose hashes differ. With 200 million keys per node and 215 leaves, one wrong key costs a walk down 15 levels and a sync of one leaf of about 6,000 keys, not 200 million.
The storage engine, in outline
A write is appended to the commit log, flushed to disk before the node confirms, and added to a sorted table in memory, later written out as an immutable sorted file. A read checks memory, then files from newest to oldest; a Bloom filter per file answers "definitely not here" without touching disk. Compaction merges files in the background.
Deletes are the trap. A delete writes a tombstone, which compaction may drop only after a grace period, typically days. If a replica was down when the delete happened and the tombstone is purged before repair reaches it, anti-entropy sees the old value on that replica, finds nothing on the others, and copies it back. The deleted key returns. So the grace period must be longer than the longest outage you repair from.
Bottlenecks and failures
At 10x (600 TB replicated, 2 million operations a second), you need about 300 nodes. Consistent hashing handles the growth, but two things strain. Hot keys do not spread: a popular key still lives on 3 nodes. And compaction debt, where writes outrun merging, makes reads check more files; watch files per read and throttle writes early.
| When this fails | What happens | What you do |
|---|---|---|
| One node, briefly | Its keys have 2 of 3 replicas. W = 2 and R = 2 still succeed. | Hinted handoff; it catches up on return. |
| One node, for good | Its 2 TB is down to 2 copies. | Declare it gone; 29 peers rebuild its ranges in about 11 minutes. |
| A whole zone | Every key loses one replica, since each key has one copy per zone. | Quorum operations continue on the other two zones. |
| A network partition | Both sides may accept writes to the same keys. | Version vectors keep both as siblings; callers merge when the link heals. |
| A disk silently corrupts data | One replica returns bad bytes. | Checksums on every block detect it; that replica re-fetches from the others. |
What a strong answer sounds like
| Level | What the answer does |
|---|---|
| Junior | Shards by hash, adds replicas, and says "eventually consistent". Needs prompting on what happens when replicas disagree. |
| Mid | Uses consistent hashing with virtual nodes, explains R + W > N, and describes read repair and hinted handoff. |
| Senior | Ties choices to the availability requirement, is precise about what quorums do not guarantee, names the cost of last-write-wins and of version vectors, separates temporary from permanent failure, and knows why tombstones need a grace period. |
With N = 3, W = 2, R = 2, is this strongly consistent?
Not in the linearizable sense. Every read overlaps every acknowledged write, so normally it sees the latest one. But concurrent writes both succeed as siblings, a write reported as failed can still show up, and if stand-ins met the quorum during an outage, a read can miss that write until the hint is delivered.
So when would you not use this design?
For anything like an account balance or a uniqueness check. Those need one leader per range with consensus; writes pause for a few seconds during failover, and that trade is right for them.
Follow-up questions
How would you run it in two regions?
Keep N replicas per region, acknowledge after a local quorum, and replicate to the other region asynchronously. Version vectors catch cross-region conflicts too; a region failure loses the last moments of unreplicated writes.
How do you support range scans?
Hashing scatters neighbouring keys. Partition by key range instead, which keeps order but needs splitting of hot or large ranges. You cannot have both with one layout.
How do you add expiry (TTL)?
Store an expiry time with the record; reads treat expired records as missing. Every replica expires it on its own, so no delete is sent, but the expired record must act as a tombstone through the same grace period. Drop it early and an older copy without the expiry, on a lagging replica, comes back through repair.
How does a new node join without downtime?
It takes about 100 new points on the ring. For each range it now owns, the current owners stream it the data while new writes go to it as well. It serves reads only once streaming ends; then the old owners drop their extra copies.
One key gets 100,000 reads a second. What now?
With R = 2, that is 200,000 replica reads on 3 nodes, about 67,000 each, over 4 times a node's normal 15,000. Cache it for a second in the coordinators, or read it with R = 1 if slight staleness is fine.
After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".
Before drawing
Numbers and interfaces
Design and depth
Failure
What to practise next
A news feed stores its timelines in a store like this one. See how fan-out decides what gets written in Design a News Feed.