Home › High-Level Design › Design a Key-Value Store
D

Design a Key-Value Store

Give it a key, it hands back a value, fast, at huge scale, even when machines are on fire. It sounds like a hash map with a network cable, and then every ugly truth about distributed systems crawls out of that one API: partitioning, replication, consistency, and failure, all at once. Candidates who pass stop treating it like storage and start treating it like a CAP argument. That is the whole interview, and they spend their 45 minutes there.

High-Level DesignHard14 min read

If you remember five things

The whole answer in one glance. Everything below is the defense of these five calls.

  1. Consistent hashing with vnodes. Partition the ring so adding a node moves one slice (not the world), and a dead node's load fans out across many neighbors instead of crushing one. Partitioning is forced by the math, not optional.
  2. Masterless, gossip-synced. No central coordinator to consult on every write, so a partition can never freeze the cluster. Any node can route any request.
  3. Tunable consistency via quorums. W + R greater than N gives a strong read, W=R=1 gives raw speed, and the caller picks per request. This is the knob the interviewer asked for.
  4. Vector clocks, not timestamps. Detect genuine concurrent writes instead of silently dropping one to a drifted clock. Tombstones keep deleted keys from rising from the dead.
  5. Repair in two places. Read repair heals on the hot path for free, Merkle-tree anti-entropy sweeps the cold path in the background. Divergence is expected, so repair is a first-class feature.

1Clarifying the requirements#

Agree on what you are building before drawing a single box. The most common way to fail this one is to build DynamoDB when the interviewer wanted Redis, or the reverse. Here is how I pin it down.

Candidate: When you say key-value store, do you mean something single-node like an embedded cache, or a distributed service that other teams call over the network?

Interviewer: Distributed. Assume it spans many machines and multiple datacenters eventually.

Candidate: Got it. What is the shape of the values? Small opaque blobs, or do we need rich types and queries?

Interviewer: Opaque blobs. No secondary indexes, no range scans across keys. Pure point lookups by key.

Candidate: And on consistency: if I write a key and immediately read it, do I have to see my write? Or can we tolerate reading a slightly stale value for a moment?

Interviewer: Good question. We want high availability above all. Eventual consistency is acceptable, but I want you to make the trade-off explicit and tunable.

Candidate: Last one. What is the value size ceiling, and is there a TTL concept?

Interviewer: Values up to about 1 MB. TTL is optional but nice to have.

What those answers just told me:

  • "High availability above all" plus "make it tunable" is the interviewer handing me the thesis: this is a Dynamo-style AP system, not a Spanner-style CP one.
  • "Opaque blobs, point lookups only" means no query engine, no secondary indexes, so I can keep the storage layer simple and spend my time on replication and consistency.
  • The interview is about quorums and conflict resolution, not about CRUD plumbing.

Functional requirements

  • get(key) returns the value or a not-found.
  • put(key, value) writes or overwrites a value, with optional TTL.
  • delete(key) removes a key.
  • Values are opaque blobs up to 1 MB.
  • No range queries, no joins, no secondary indexes.

Non-functional requirements

  • Highly available. The store answers even during node and datacenter failures.
  • Low latency. Single-digit millisecond reads and writes at p99.
  • Horizontally scalable. Add nodes, get more capacity, no downtime.
  • Tunable consistency. Callers choose how strict they want to be, per request.
  • Durable. An acknowledged write is not silently lost.
From the interviewer's chair

That last pair, tunable consistency plus durability, is where the Hard rating lives. An always-consistent system is easy to reason about but dies under partition. An always-available system stays up but hands back stale or conflicting data. I am asking you to ship both knobs and route the choice to the caller. If you start drawing boxes before you have named that tension out loud, I have quietly downgraded you.

2Back-of-the-envelope estimation#

Size it so the decisions have numbers behind them, not vibes. Two numbers decide the whole architecture, and I will flag them when I reach them.

The point of this section

You are not here to compute petabytes to three decimals. You are here to surface two load-bearing facts: a read is too fast to ever touch spinning disk or a cross-datacenter hop, and 500 TB does not fit on one machine. The first forbids slow reads, the second forces partitioning. Find both fast, then move on.

Users and traffic.

  • 200 million daily active users, each touching the store about 50 times a day (sessions, preferences, flags, cached objects).
  • That is 10 billion operations a day.
10,000,000,000 ops / 86,400 sec ≈ 115,000 ops/sec average
  • Traffic is spiky, so plan for 3x peak: roughly 350,000 ops/sec.
  • Read-heavy like most stores, call it a 9:1 split.
Reads:  ~315,000 QPS
Writes:  ~35,000 QPS

Storage.

  • Say 50 billion keys. Average value about 10 KB (most small, a few near 1 MB), plus ~100 bytes of key and metadata.
50,000,000,000 keys × ~10.1 KB ≈ 500 TB of logical data
  • Replicate 3x for durability and availability: 1.5 PB raw before compression.
  • With a 2:1 compression ratio on real blobs, about 750 TB on disk.

Bandwidth (where the network actually gets hot).

35,000 writes/sec × 10 KB ≈ 350 MB/sec ingest
  • Multiply by replication factor 3 for cross-node traffic, and you are pushing north of 1 GB/sec on the internal network at peak.
  • That tells me the replication path, not the client path, is where the network burns. I come back to it in the trade-offs.

The two numbers that drive the design.

  • 315,000 read QPS at single-digit-millisecond p99. This forbids spinning disk on the read path (a seek is ~10 ms and blows the whole budget alone) and forbids a cold cross-datacenter hop on a normal read (tens of milliseconds by itself). Every normal read serves from memory or local SSD, in the local datacenter.
  • 500 TB. It does not fit on one machine, not close, so partitioning is mandatory, not a nice-to-have.
315K QPS
The number that sets the latency floorReads per second at single-digit-millisecond p99. It is why reads never touch spinning disk or a cross-DC hop, and why hot content lives in the coordinator cache. Remember this one; it comes back in every trade-off.

3Core APIs#

Keep the surface tiny. A small API is a feature here, not laziness: fewer operations means fewer consistency corner cases to reason about.

GET /v1/keys/{key}?consistency=one
200 OK
{ "key": "user:42:prefs", "value": "<base64 blob>", "version": "v17-node3" }

PUT /v1/keys/{key}
{ "value": "<base64 blob>", "ttl_seconds": 3600, "consistency": "quorum" }
200 OK
{ "version": "v18-node3" }

DELETE /v1/keys/{key}?consistency=quorum
204 No Content

The consistency param is the knob the interviewer asked for.

  • one acks after a single replica responds (fastest, weakest).
  • quorum needs a majority to agree (slower, stronger).
  • all waits for every replica.
  • Same store, same code path, caller's call: a session cache sends one and eats the odd stale read for latency, a billing flag sends quorum and pays for correctness.

The version field is not cosmetic.

  • It is how clients and the server detect conflicting writes.
  • It carries the vector clock the server uses to decide which value is newer. More on that in the deep dive.

4High-level design#

The point of this section

The box diagram is table stakes. What earns points is one structural choice: there is no master. No node has to be consulted on every write, so a partition can never freeze the cluster. Draw the boxes fast, then defend the absence of a master, because that absence is the entire availability story.

The spine is simple, so draw it fast:

  • Clients hit a stateless coordinator layer.
  • The coordinator figures out which nodes own the key and talks to them.
  • Storage nodes hold the data, replicate among themselves, and gossip about who is alive.
flowchart LR C[Client] --> LB[Load Balancer] LB --> CO[Coordinator] CO --> R[Hash Ring Router] R --> N1[(Storage Node A)] R --> N2[(Storage Node B)] R --> N3[(Storage Node C)] N1 -. gossip .- N2 N2 -. gossip .- N3 N3 -. gossip .- N1
The whole system at a glance. The choice that matters is what is missing: there is no central master. Every node learns the ring through gossip, so any node can coordinate any request, and losing a node takes nothing permanent down.

No central master.

  • No single node owns metadata and becomes a bottleneck or a single point of failure.
  • Every node knows the ring topology through gossip, so any node can coordinate any request.
  • The moment you add a master that must be consulted on every write, you have handed a partition the power to freeze the whole cluster. That is the opposite of what the interviewer asked for.

The coordinator is stateless.

  • It is just routing logic: read the ring, forward to the right replicas, collect responses, apply the consistency rule, answer.
  • Run hundreds behind the load balancer and lose any of them with no data loss.
  • At 350K ops/sec this layer scales by adding boxes, nothing clever.

Storage nodes own ranges of the keyspace via consistent hashing.

  • Each node owns a slice of the ring plus replicas of its neighbors' slices.
  • When a node joins or leaves, only the keys near it on the ring move. That is the whole point: adding a node does not reshuffle the dataset.

Under the hood, each node runs an LSM-tree engine.

  • Writes go to an in-memory memtable plus an append-only commit log, then flush to sorted immutable files on SSD.
  • Fast writes and good point-lookup reads, which is exactly our workload.
  • The append-only write path is what lets one node absorb its share of 35K writes/sec without random-write disk thrash.

5Data model#

No schema in the relational sense, but each node tracks structure per key. Here is a stored record internally.

Field Type Notes
key bytes Up to 100 bytes. Hashed to place on the ring.
value bytes Opaque blob up to 1 MB. Compressed at rest.
vector_clock map node to counter Tracks causal history for conflict detection.
timestamp int64 Last-write wall-clock millis, tiebreaker only.
ttl int64 or null Expiry epoch. Null means never.
tombstone bool Marks a delete until compaction reclaims it.

And the ring metadata every node carries, synced via gossip.

Field Type Notes
node_id string Stable identity, survives restarts.
token_ranges list of ranges Which hash ranges this node owns.
status enum up, suspect, down, joining, leaving.
heartbeat int64 Logical clock bumped every gossip round.
datacenter string Used for rack-aware and DC-aware replica placement.

Two fields decide interviews, so call them out before you are asked.

  • tombstone. In an eventually consistent system you cannot just erase on delete. If node C was offline during the delete and you physically removed the key on A and B, then C comes back, gossips the old value, and resurrects the dead key. The tombstone is a gravestone that outlives the downtime, then gets garbage-collected during compaction after a grace window.
  • vector_clock. The real workhorse for consistency, so it gets its own deep dive below.

Name both of these before I ask and I know you have actually operated one of these systems.

6Deep dives#

6.1 Partitioning with consistent hashing and virtual nodes#

Naive hashing is a trap.

  • node = hash(key) % N remaps nearly every key when N changes by one.
  • At our scale, adding one machine would move hundreds of TB. Unacceptable.

Consistent hashing fixes it.

  • Picture the hash output as a ring from 0 to 2^128. Each node gets a position.
  • A key hashes to a point, then you walk clockwise to the first node: that node owns it.
  • Add a node and only the keys between it and its predecessor move. One slice, not everything.

Plain consistent hashing has two problems: uneven load, and a hot neighbor inheriting everything when a node dies. Virtual nodes fix both.

  • Each physical machine takes many positions on the ring, say 256.
  • Load now spreads evenly across the cluster.
  • When a machine dies, its 256 slices get absorbed by 256 different neighbors instead of dumping onto one.
  • That last part is the operational win people miss: without vnodes, losing one box doubles the load on exactly one other box and your outage cascades. With 256 vnodes, the dead box's load fans out and nobody notices.
flowchart TD K[Key hashed] --> P[Point on ring] P --> W[Walk clockwise] W --> V1[VNode owner] V1 --> M1[Physical node 3] W --> V2[Next VNode replica] V2 --> M2[Physical node 7] W --> V3[Next VNode replica] V3 --> M3[Physical node 1]
A key lands on a point, walks clockwise, and the next N distinct physical nodes become its replica set. Note how three vnodes resolve to three different physical machines, which is the rule that keeps a replica set off one box or one rack.

Replica placement has one rule you must say out loud.

  • Replicas are the next N distinct physical nodes walking clockwise.
  • Skip vnodes that land on a physical machine already chosen, and prefer spreading across datacenters.
  • So a replica set never ends up as three vnodes on the same box or the same rack. Lose a rack, you still have copies.
  • Skipping that rule is the quiet bug that turns a rack-power event into a data-loss event.

6.2 Tunable consistency with quorums#

This is the heart of the Hard version and the single deep dive I most want to hear nailed. We promised eventual consistency but a tunable knob. Quorum math delivers it.

The rule. Let N be the replication factor (3 for us), W the write quorum, R the read quorum.

W + R > N

Strong reads: W=2, R=2, N=3.

  • A write is not acked until 2 of 3 replicas confirm. A read collects answers from 2 of 3.
  • Because 2 + 2 is greater than 3, any read quorum and any write quorum must overlap on at least one node.
  • So a read is guaranteed to touch at least one replica holding the newest value. Its higher version wins.
  • Strong consistency, and we still survive one dead replica.

Raw speed: W=1, R=1.

  • Writes ack instantly, reads return the first answer.
  • Now W+R is not greater than N, so a read can miss a fresh write.
  • That is the consistency=one path, chosen by the caller with eyes open.
sequenceDiagram participant C as Client participant CO as Coordinator participant A as Replica A participant B as Replica B participant D as Replica D C->>CO: PUT key quorum W2 CO->>A: write v18 CO->>B: write v18 CO->>D: write v18 A-->>CO: ack B-->>CO: ack CO-->>C: 200 OK two acks is quorum D-->>CO: ack later or never
A quorum write with one lagging replica. The coordinator answers the instant two replicas confirm; replica D can be slow or dead and never blocks the write. D catches up afterward through read repair and anti-entropy.
Pause before you read on: with N=3, you set W=3 and R=1 to make reads cheap. The math still satisfies W+R greater than N. What did you just give up?
Reveal the answer
Availability on writes. W=3 means every single write must reach all three replicas, so the moment one replica is down or partitioned, writes stop entirely. You traded the system's whole availability promise for a marginally cheaper read. W=2, R=2 keeps both reads and writes alive through one failure, which is why it is the sane default. Reaching for the balanced quorum unprompted is what earns the room.

D catches up through two mechanisms.

  • Read repair. A read that notices D is stale pushes the fresh value to it inline, so the system self-heals on the hot path at essentially no extra cost.
  • Anti-entropy. A background process where nodes compare Merkle trees of their key ranges and sync only the differing branches.
  • Merkle trees matter because you cannot compare billions of keys one by one. You compare tree-root hashes and only descend into subtrees whose hashes disagree.
  • A full range with one divergent key costs a logarithmic walk, not a full scan. That is what makes repair affordable at 50 billion keys.

6.3 Conflict resolution and the resurrection problem#

The setup. With W=1, two clients write the same key to different replicas during a partition. Two divergent values, no global order. Who wins?

Wall-clock timestamps alone lie.

  • Clocks drift across machines.
  • Last-write-wins by timestamp silently drops data when two writes race: the one with the slightly slower clock just vanishes.

So each value carries a vector clock: a map of node-id to a counter.

  • Node A's write makes it {A:1}. A later causally-dependent write becomes {A:2}.
  • Two concurrent writes on a partition become {A:1} and {B:1}, which are not comparable, because neither map is element-wise greater than the other.
  • The system sees that neither clock descends from the other and knows it has a genuine conflict, not a stale copy.

What then? Two honest options.

  • Last-write-wins by timestamp. Simple but lossy, fine for caches and preferences where losing one racing write costs nothing.
  • Siblings. Hand both versions back to the client and let application logic merge them, the way a shopping cart unions items so nothing a user added disappears.
  • I default to last-write-wins for a general store and offer sibling mode for callers who opt in, because most teams do not want to write merge logic and take the simpler contract every time.

The resurrection problem closes here, cleanly, thanks to vector clocks.

  • A delete produces a tombstone with its own vector clock.
  • When a recovering node gossips its stale live value, that value's clock is an ancestor of the tombstone's clock, so the tombstone correctly wins and the key stays dead.
  • Garbage-collect the tombstone only after a grace window longer than the maximum expected node downtime. Collecting it early is exactly how a deleted key crawls back out of the grave.
Red flag that fails you here

Reaching for last-write-wins by wall-clock timestamp as your only conflict story, then going quiet when I say "two clocks drifted by 200 ms and the later write had the earlier timestamp." Name vector clocks on your own, explain why two incomparable clocks mean a real conflict, and say how you would resolve it. Candidates who never get past timestamps are the ones who have not watched this system lose data.

7Bottlenecks and trade-offs#

Hot keys are the one thing consistent hashing does nothing for.

  • A viral post's view counter, a global feature flag read by every request: all its traffic lands on the same 3 replicas and melts them.
  • Adding nodes does not help, because the hash still points those reads at the same three boxes.
  • My defenses, in the order I would reach for them:
  • Cache hot keys in the coordinator with a short TTL, so repeated reads never reach storage. Cheapest, so first.
  • Detect hotness by tracking per-key request rates and add extra ephemeral replicas for that single key, so its read load fans out.
  • For write-hot counters, shard the key into key#0 through key#9 across different ring positions and sum on read, turning one melting-hot key into ten warm ones.
  • Each defense trades a little consistency or complexity for survival.

The CAP trade-off, stated plainly.

  • During a partition we chose availability. A partitioned minority still serves reads and writes, and those diverge from the majority.
  • We accept temporary inconsistency and repair it with read repair and anti-entropy.
  • If the interviewer flips the requirement to strong consistency during partitions, I move to a consensus protocol like Raft per partition. That buys linearizable reads and writes but costs availability: a partition with no quorum refuses writes rather than risk divergence.
  • You cannot have both at once. I let the caller choose at the keyspace level, so the session cache stays AP and the billing ledger goes CP.

Tail latency from the slowest replica.

  • With R=2 we wait for the second-fastest of three, fine on a healthy day.
  • But a single slow node drags p99 everywhere it is a replica, and one sick machine out of thousands can wreck a latency SLA.
  • Send the read to all N replicas, answer as soon as R respond, cancel the laggard, and run speculative retries to a different replica past a threshold of a few milliseconds.
  • Cheap insurance, and it is the difference between a clean p99 and a bimodal one.

Compaction and write amplification.

  • LSM-trees rewrite data during compaction. At 35K writes/sec that is real disk and CPU, and a badly timed compaction spikes latency for every read sharing that node.
  • Rate-limit compaction, run it off-peak where possible, and watch the classic failure where deletes and overwrites bloat storage until compaction catches up and the disk fills faster than you expected.

Cross-datacenter replication (where the estimation warned the network gets hot).

  • Synchronous cross-DC writes add tens of milliseconds and kill the single-digit-millisecond target.
  • So cross-DC replication is asynchronous: ack from the local DC quorum, ship to remote DCs in the background.
  • The trade-off: a remote DC can lag, and a full-DC failure right after a write can lose the last few seconds of data.
  • For most workloads that is acceptable. For the few keys that cannot tolerate it, offer a synchronous all-dc level and let the caller pay the latency they are asking for.

Always route the consistency choice to the caller, not to your own taste.

8Summary#

The decisions that matter:

  • Consistent hashing plus virtual nodes. Adding capacity moves one slice, not the world, and a dead node's load spreads across many neighbors instead of crushing one.
  • Masterless, gossip-synced. Any node can coordinate and no single failure takes us down, which is the entire availability story.
  • LSM-trees on SSD. Fast writes and point reads, which is what keeps us inside the 315K-read-QPS-at-millisecond-p99 budget.
  • Tunable consistency backed by quorum math. W + R greater than N gives strong reads, W=R=1 gives raw speed, caller chooses per request.
  • Vector clocks, not naked timestamps, to detect real conflicts, plus tombstones so deleted keys stay dead.
  • Repair in two places: read repair inline on the hot path, Merkle-tree anti-entropy in the background.
  • Honest about CAP: we chose availability, we repair after partitions, and we offer a stricter per-keyspace mode for the keys that need it.
Follow-ups the interviewer will ask (and your answers)
"Two clients write the same key during a partition. How do you resolve it without losing data?"
Each value carries a vector clock, a map of node-id to counter. Two concurrent writes produce incomparable clocks ({A:1} and {B:1}, neither element-wise greater), so the system knows it is a genuine conflict, not a stale copy. I default to last-write-wins by timestamp for a general store, and offer sibling mode (hand both back, let the app merge, like a shopping cart union) for callers who cannot afford to drop a write.
"A node was offline during a delete, then comes back. Why does the key not resurrect?"
The delete leaves a tombstone with its own vector clock. When the recovering node gossips its stale live value, that value's clock is an ancestor of the tombstone's, so the tombstone wins and the key stays dead. I only garbage-collect the tombstone after a grace window longer than the maximum expected node downtime, because collecting it early is exactly how a deleted key crawls back out.
"One key goes viral and melts its three replicas. Adding nodes does not help. Now what?"
Right, the hash still points every read at the same three boxes. In order: cache the key in the coordinator with a short TTL so most reads never reach storage; detect the hotness by per-key rate and spin up extra ephemeral replicas to fan the reads out; and for a write-hot counter, shard it into key#0 through key#9 on different ring positions and sum on read, turning one hot key into ten warm ones.
"How do two nodes reconcile billions of keys without scanning everything?"
Merkle trees. Each node keeps a hash tree over its key ranges. To compare, they exchange root hashes and only descend into subtrees whose hashes disagree. A range with one divergent key costs a logarithmic walk, not a full scan, which is what makes background anti-entropy affordable at 50 billion keys.
"The interviewer now demands strong consistency during a partition. What changes?"
I move that keyspace to a consensus protocol like Raft per partition, which gives linearizable reads and writes. The cost is availability: a partition with no quorum refuses writes rather than risk divergence. You cannot have AP and CP on the same data at once, so I let the caller pick at the keyspace level, session cache stays AP, billing ledger goes CP.

What you can now explain in the room

  • Why 315K reads/second at millisecond p99 forbids spinning disk and cross-DC hops on the read path.
  • Why consistent hashing plus virtual nodes beats modulo hashing, and why vnodes stop a cascade when a box dies.
  • How W + R greater than N guarantees a strong read, and what W=R=1 gives up.
  • Why vector clocks detect real conflicts that wall-clock timestamps silently lose.
  • How tombstones and a grace window stop a deleted key from resurrecting after a node recovers.
  • Why you chose availability under partition, and exactly how you would flip a keyspace to CP when asked.

For role-specific angles on this problem, see our company-specific system design guides.

Get a prep plan built around your Design a Key-Value Store interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Design a Key-Value Store prep plan is on the way.