System Design Road

Hard Interview walkthrough

Distributed key-value store: the interview, played out

This question asks you to build the database everyone else’s designs depend on. It sounds intimidating, but it comes down to four questions: where does each key live, how many copies, what happens when copies disagree, and what happens when machines fail. We’ll answer them one at a time.

10 min read8 interactive diagrams12 sections

This gives away every stage. For a clean attempt, practise it first, then come back.

The short version

  • With about 110 machines, something is always broken. Failure handling is the normal case, not an edge case.
  • Consistent hashing with virtual nodes decides where each key lives, and moves little data when machines come and go.
  • Three copies, and quorums (W + R > N) let each request trade speed for freshness.
  • To stay writable when a replica is down, write to a stand-in and hand the data over later.
  • Copies drift, so repair them: on reads, with handoffs, and with background comparisons.

The 45 minutes

Before we start

In every other interview question, you say “store it in a key-value store like DynamoDB” and move on. This time, you’re building the store.

The interface is tiny: put a value under a key, get it back, delete it. Everything interesting is underneath. The data is too big for one machine, so it must be spread out. Machines fail every day, so there must be copies. Copies can disagree, so you need rules for who wins. And all of it must answer in a few milliseconds.

This design follows Amazon’s Dynamo paper, the idea behind DynamoDB and Cassandra. You don’t need to have read it; we’ll build it step by step.

Same order as always: requirements, quick maths, data and API, a design that grows, then deep dives.

Minutes 0–5Requirements

Interviewer

Interviewer: Design a distributed key-value store, something like DynamoDB or Cassandra.

You

You: What operations do we need: get, put, delete? Range queries?

Interviewer

Interviewer: Get, put and delete by key. No range queries or transactions.

You

You: How much data, how big are values, and how much traffic?

Interviewer

Interviewer: About 100 TB, values usually around 10 KB, up to 1 MB. About a million operations a second.

You

You: When machines or the network fail, should we keep accepting writes, or refuse them to stay perfectly consistent?

Interviewer

Interviewer: Stay available. Think of a shopping cart: always let people add items, and sort out disagreements later.

You

You: Can clients ask for more up-to-date reads when they need them?

Interviewer

Interviewer: Yes, letting them choose per request would be good.

You

You: How many copies, and where?

Interviewer

Interviewer: Three copies, in three zones of one region.

You

You: And we need to add machines without downtime?

Interviewer

Interviewer: Yes.

Functional requirements

  1. Put, get and delete values by key.
  2. Let each request choose between faster and more up to date.
  3. Add or remove machines without downtime, moving as little data as possible.

Nice to have: values that expire after a set time. Out of scope: range queries, secondary indexes and transactions.

Non-functional requirements

  1. Fast: a few milliseconds per request, even at the 99th percentile.
  2. Always writable: writes keep working when machines or network links fail.
  3. Durable: three copies in three zones, so even losing a whole zone loses nothing.
  4. Big: 100 TB of data and a million operations a second.
  5. Copies catch up: after failures, all copies eventually agree again.

Minutes 5–10Back-of-the-envelope estimates

You

You: 100 TB with three copies is 300 TB stored. If each machine has 4 TB of usable disk and we keep disks about 70% full, that’s roughly 110 machines.

You: A million operations a second, each touching three copies, spread over 110 machines, is about 27,000 a second per machine. Comfortable.

You: At 10 KB per value, 100 TB is around 10 billion keys.

WhatWorkingResult
Stored100 TB × 3 copies300 TB
Machines300 TB ÷ (4 TB × 70% full)≈ 110 machines
Load per machine1M operations × 3 copies ÷ 110≈ 27,000 a second
Keys100 TB ÷ 10 KB≈ 10 billion

Two things follow. First, with 110 machines, one of them is failing or being updated almost every day. Failures aren’t rare edge cases here; they’re the normal state. Second, with 10 billion keys, we can’t keep a central list of where each one lives. The location has to be worked out from the key itself.

Minutes 10–15Core entities and the API

Core entities

  • Key and value: the key is a string; the value is opaque bytes up to 1 MB.
  • Version: a small record attached to each value, used to tell which of two copies is newer.
  • Node: one machine, holding part of the data and part of the copies.
  • Ring: how keys are assigned to nodes (explained below), shared by every node.

The API

PUT/v1/kv/cart:user_42

Store a value. W says how many copies must confirm before replying.

Query
w = 2 (of 3 copies)
Body
{ "items": ["book", "pen"] }
Responses
  • 200{ "version": "v7" }
GET/v1/kv/cart:user_42

Read a value. R says how many copies to ask.

Query
r = 2 (of 3 copies)
Responses
  • 200{ "value": { "items": ["book", "pen"] }, "version": "v7" }
  • 404No such key.
DELETE/v1/kv/cart:user_42

Delete a key. Stored as a “deleted” marker, so stale copies can’t bring it back.

Responses
  • 204Deleted.

The two small numbers, W and R, are how each request picks “faster” or “more up to date”. We’ll see exactly what they mean in stage 3.

Minutes 15–25The design, built up

Stage 1One machine: a hash map, a log, and snapshots

ClientsOne serverkey → value in memoryDiskwrite-ahead log + snapshotsget / putappend every writeFast and simple.But 100 TB won't fit, and one crash means downtime.
84%
You

You: With no scale at all: one server keeps a hash map of key to value in memory. Every write is first appended to a log file on disk, so after a crash we can replay the log and rebuild the map. Every so often we save a full snapshot so the log doesn’t grow forever.

This is fast and simple, and plenty of real systems start here. But 100 TB won’t fit on one machine, and when it crashes or reboots, everything is unavailable. Both break our requirements.

Stage 2Spread keys across machines with consistent hashing

Client libraryknows the ringCoordinatorhash the key, find its nodeNEWNode Akeys 0–33%Node Bkeys 33–66%Node Ckeys 66–100%this key lives here
94%
Green: new in this stage
You

You: First, split the data. The obvious way is hash the key and take it modulo the number of machines. But then adding one machine changes the answer for almost every key, and nearly all the data has to move.

You: Instead, consistent hashing: imagine a circle. Each machine is placed at points on the circle by hashing its name, and each key is placed by hashing the key. A key belongs to the next machine clockwise. Adding a machine only takes keys from its neighbours.

You: And with virtual nodes, each machine appears at many points around the circle, say 256. That spreads load evenly and means a joining machine takes small slices from many machines instead of a big chunk from one.

Lesson 5.2 shows how much data moves each way:

Adding a fifth server
5 servers
60 cache keys, coloured by serverKeys per serverserver 19server 28server 314server 420server 5947 of 60 keys moved (78%)

Most keys moved, because almost every key’s remainder changed when N went from 4 to 5. For a cache, every moved key is a miss.

Placement

Stage 3Three copies and quorums

Client libraryknows the ringCoordinatorany node for this keyReplica 1zone ANEWReplica 2zone BNEWReplica 3zone CNEWget / putW or R of 3
101%
Green: new in this stage
You

You: Next, copies. Each key is stored on the first three distinct machines clockwise from it, and we make sure they’re in three different zones.

You: For a write, the coordinator sends it to all three and replies as soon as W of them confirm. For a read, it asks R of them and returns the newest version.

You: If W plus R is more than 3, every read overlaps with every write on at least one copy, so reads see the latest successful write. W = 2 and R = 2 is the balanced default. W = 1, R = 1 is fastest, but might return slightly old data. Each request can choose.

That overlap rule is easier to see than to explain. Lesson 5.3 has a demo:

Three replicas, a write, then a read
R + W = 4 > N
Writersets bio = v2Readerreads v2 ✓replica Av2replica Bv2replica Cv1

R + W > N, so the read and write sets overlap in at least one replica, and the read sees v2. (Still not full linearizability: concurrent writes need another rule.)

W
R

Stage 4When machines fail: gossip, hinted handoff, repair

Client libraryknows the ringCoordinatorany node for this keyReplica 1zone AReplica 2zone BReplica 3zone CHinted handoffwrites for a down nodeNEWGossipwho's up, ring layoutNEWAnti-entropycompare copies (Merkle trees)NEWStorage enginelog + LSM on each nodeNEWget / putW or R of 3if a replica is down
71%
Green: new in this stage
You

You: Now failures, which we said are normal. Four pieces.

You: Gossip: every second, each node chats with a few random others, sharing who’s up and the ring layout. News spreads to the whole cluster in seconds, with no central coordinator to fail.

You: Hinted handoff: if a replica is down, the coordinator writes to the next healthy node instead, with a note saying who the data really belongs to. When the replica comes back, the data is handed over. So writes never fail just because one machine is rebooting.

You: Repair: when a read sees copies disagree, it updates the stale one. And in the background, replicas compare their data to find and fix anything that drifted.

You: On each node, data lives in a write-friendly storage engine: a log, an in-memory table, and sorted files on disk.

Here’s hinted handoff, compared with simply waiting for all three:

A write while one of three replicas is down
+2 s
Clienterror: try againCoordinatortimed outReplica Azone AReplica Bzone BReplica Cdown for 10 minutesNode Dstand-in

Requiring all three replicas means the write fails while any one machine is down. With about 110 machines, one is almost always down somewhere. That breaks 'always writable'.

When a replica is down

Checking the design against the requirements

RequirementWhere the design handles it
Put, get, deleteCoordinator routes each request to the key’s three replicas
Choose faster or more up to dateW and R per request
Add or remove machines without downtimeConsistent hashing with virtual nodes; small slices move
FastNo central coordinator; one hop to replicas; write-friendly storage
Always writableStand-in nodes and hinted handoff when a replica is down
DurableThree copies in three zones; every write logged first
BigAbout 110 machines, each doing ~27,000 operations a second
Copies catch upRead repair, hinted handoff, background comparison

Deep diveWhere does each key live?

Interviewer

Interviewer: Why not just hash mod N?

Bad solutionHash mod N

hash(key) % 110. Simple, until machine 111 arrives and almost every key now maps somewhere else. Nearly all 300 TB would have to move, and while it does, reads land on the wrong machines.

Good solutionConsistent hashing

Keys and machines on a circle; each key goes to the next machine clockwise. A new machine only takes keys from its immediate neighbour, about 1/110th of the data.

Great solutionConsistent hashing with virtual nodes

Each machine appears at, say, 256 points around the circle. Data spreads evenly even with uneven luck in hashing, a new machine takes small slices from many machines at once (so no single machine is overloaded moving data), and more powerful machines can simply be given more points. Lesson 5.2 shows data moving gradually instead of all at once:

Moving logical shard 1873 from db-07 to db-33
switched
App serversdb-07 (old)db-33 (new)copydb-07: “moved to db-33”routing: 1873 → db-33 (v2)1 Snapshot2 Catch up3 Switch4 Verify5 Old map?

One shard of 4,096 paused for 280 ms. No writes were lost, and a stale client was redirected instead of writing to a copy that’s about to be deleted.

Deep diveWhen copies disagree

Interviewer

Interviewer: Two clients update the same cart at the same time, through different replicas. Which value wins?

Bad solutionLast write wins, by the clock

Keep whichever value has the newest timestamp. Simple, but machine clocks drift, so the write that really happened second can lose, and the lost write disappears without anyone noticing. For a shopping cart, an item silently vanishes.

Good solutionVersion vectors

Each value carries a small record of which replicas have updated it and how many times. Comparing two records tells you whether one version came after the other (keep the newer one) or whether they happened at the same time without knowing about each other. In that case, keep both and let the application merge them. Amazon’s Dynamo did exactly this for carts: when two versions conflict, merge them by keeping the items from both.

Great solutionPick per use case

For some data, last write wins is genuinely fine, like “last known location”. For carts, use version vectors and merge. For counters and sets, use data types designed to merge automatically, called CRDTs. The strong answer is naming what each choice can lose. Lesson 5.3 shows last write wins losing data:

Priya edits her bio on two devices
1 edit lost
Real time10:00:0010:00:0110:00:0210:00:0310:00:04Server Aclock 3 s fastServer Bclock correct“Chai lover” · stamped :03“Coffee convert” · stamped :01Kept “Chai lover” (higher timestamp). Her real last edit is gone.

The earlier edit wins, because a fast clock gave it a later timestamp. Last-write-wins quietly discarded what Priya actually changed it to.

On conflict

Deep diveCopies drifting apart

Interviewer

Interviewer: After all these failures, how do you make sure the three copies eventually match?

You

You: Three layers, from cheapest to most thorough.

You: Read repair: whenever a read sees replicas disagree, it sends the newest version to the stale ones. Popular keys get fixed constantly, for free.

You: Hinted handoff: writes held by a stand-in are delivered when the real replica returns.

You: Anti-entropy: in the background, replicas compare their data using Merkle trees, which are trees of hashes over ranges of keys. If two replicas’ top hashes match, everything matches. If not, they walk down the tree to find exactly which keys differ, without sending all the data across the network.

One subtle trap: deletes. If a replica missed a delete, it still has the old value, and read repair would copy it back. So a delete is stored as a marker (a “tombstone”), kept long enough for all copies to see it, and only then cleaned away.

Deep diveInside each node

Interviewer

Interviewer: What does a single node use to store data on disk?

You

You: A write-friendly design. Every write is appended to a log on disk first, so nothing is lost in a crash. Then it goes into a sorted table in memory. When that fills up, it’s written out as a sorted file, and background work merges files together over time.

You: Reads check memory first, then the files from newest to oldest, and use a Bloom filter per file to skip files that can’t contain the key.

You: This is the LSM-tree design from lesson 3.2, used by Cassandra and RocksDB. It suits us because writes are just appends, which disks handle very fast.

40 notifications arrive
written to disk ≈ 268 KB
Index pages on disk (each 8 KB)Write-ahead logapp wrote 3.9 KB · 33 different pages changed

Each 100-byte notification dirtied a different 8 KB page, scattered across the disk. About 69× more bytes reach the disk than the app wrote (illustrative).

Engine

Deep diveThe CAP choice, made concrete

Interviewer

Interviewer: You keep saying “always writable”. What does that cost?

You

You: During a network split, some replicas can’t reach others. We choose to keep accepting writes on both sides, using stand-ins if needed. The cost is that, for a while, the two sides may hold different versions, and a read might return an older one. When the split heals, repair and conflict resolution bring them back together.

You: The other choice would refuse writes on the side without a majority. That’s what systems built on consensus do, like etcd. Our interviewer chose availability, the shopping-cart choice, so we accept temporary disagreement in exchange for never saying no.

Lesson 5.4 shows a network split playing out both ways:

Zone B is cut off for ninety seconds
consistent, partly unavailable
Zone AleaderZone Bcut off (minority)Zone Creplica✕✕Priya likes a posterror: try againArjun claims @pune_rainserror: try againSana posts a photoerror: try againZone A and C users:working normally ✓

Nothing can go wrong, but zone B’s users get errors for 90 seconds, even for likes, which could safely have waited. Better: choose per feature.

When the leader can’t be reached

The complete architecture

Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.

Client libraryknows the ringCoordinatorany node for this keyReplica 1zone AReplica 2zone BReplica 3zone CHinted handoffwrites for a down nodeGossipwho's up, ring layoutAnti-entropycompare copies (Merkle trees)Storage enginelog + LSM on each nodeget / putW or R of 3if a replica is down
71%

How one request travels through it:

  1. The client library knows the ring, so it sends a request straight to a node responsible for the key. That node coordinates the request.
  2. The coordinator hashes the key to find its three replicas: the next three distinct machines around the ring, placed in three different zones.
  3. For a write, it sends the value to all three and replies once W of them (say 2) confirm. Each replica appends the write to its log first, then its in-memory table.
  4. For a read, it asks R of them (say 2) and returns the newest version. If one replica had an older version, it quietly updates it (read repair).
  5. If a replica is down, another node accepts the write with a note saying who it’s for, and hands it over when the replica comes back.
  6. Nodes gossip to share who’s up and how the ring is laid out, and a background process compares copies to fix anything that drifted.

How this answer gets scored

Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.

MomentWhat earns creditWhat costs you
The first five minutesAsking “accept writes or stay consistent?”Promising both perfect consistency and availability
Placing keysConsistent hashing with virtual nodesHash mod N
CopiesThree replicas across zones, W + R > N explained“We’ll replicate it” with no detail
ConflictsComparing approaches and naming what each losesLast write wins with no mention of clocks
FailuresStand-ins, hinted handoff, read repair, Merkle treesWrites failing whenever one machine is down
DeletesTombstones, so stale copies can’t bring data backDeleting the value outright

What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.