← System design

Storage & Data

Design Key-Value Store

A key-value store promises almost nothing: give it a key, get back bytes. That small promise is why so many systems sit on top of one, and why it has to be fast at the tail, correct under failure and boring to operate. The design questions are where each key lives, how many copies exist and who decides which copy is right, and how bytes are laid out on disk so writes stay cheap.
Core: hash partitions + per-partition Raft + LSM storage4M operations a second at peak60 TB of live datap99 under 10 ms inside a region

01

Scope and targets

02

Scope and targets

Functional
  • get, put and delete on items up to 400 KB, grouped into named tables.
  • Composite keys: a partition key plus an optional sort key, with range queries over the sort key inside one partition key.
  • Conditional writes (put only if absent, or only if the version matches) for safe read-modify-write.
  • Per-request choice of strong or eventual read consistency.
  • Time-to-live per item, batch get and batch write.
  • A change stream of item updates for downstream consumers, and point-in-time backups.
Non-Functional
  • p99 under 10 ms for single-item reads and writes within a region.
  • Acknowledged writes survive the loss of any one availability zone.
  • Capacity grows by adding nodes, with no downtime and no client changes.
  • Throughput scales linearly with node count for well-distributed keys.
  • One table's traffic spike must not degrade other tables on shared hardware.
  • Available for writes through single-node and single-zone failures, 99.99% overall.
Where the interview usually forks

Ask early whether clients need linearizable reads and conditional writes, or whether last-writer-wins with eventual convergence is acceptable. The first points you to a leader per partition with a consensus log. The second allows a leaderless, Dynamo-style design with tunable quorums. Both are valid; mixing their vocabulary without choosing is the most common mistake. This walkthrough chooses the first, then explains where the second wins.

02

Sizing the cluster

03

Sizing the cluster

Assume a multi-tenant regional service with 4 million operations a second at peak, 70% of them reads, 40 billion items averaging 1.5 KB, and three replicas of everything.

QuantityWorkingResult
Peak reads4M x 70%2.8M / s
Peak writes4M x 30%1.2M / s
Replica write operations1.2M x 3 copies3.6M / s
Logical data40B items x 1.5 KB60 TB
Stored bytes60 TB x 3 replicas x 1.2 LSM space overheadabout 216 TB
Partitions60 TB / 10 GB target sizeabout 6,000, so 18,000 replicas
Nodes for storage216 TB / (4 TB NVMe x 60% fill target)about 90
Nodes for throughput(2.8M + 3.6M) / about 80K ops per nodeabout 80
Provisioned fleetmax of the two, plus a zone of headroomabout 130 nodes, 140 replicas each
Write bandwidth into the WAL3.6M x 1.6 KBabout 5.8 GB / s cluster wide
Storage and throughput agree

Both lines point to roughly a hundred nodes, which is a healthy sign: neither disks nor CPUs are idle. If one had come out ten times larger, you would change the node shape (more disk, or more cores) instead of buying idle hardware.

Averages hide the real problem

80,000 operations a second per node assumes keys spread evenly. A single hot partition key can concentrate tens of thousands of requests on one partition and one leader. Hot keys, not total load, cause most production incidents in stores like this.

03

Client interface

04

Client interface

PUT/v1/tables/orders/items/{pk}/{sk}Write one item, optionally conditional
// pk = customer#4410, sk = 2026-10-01#ord_88a { "value": { "total": 2499, "status": "paid" }, "condition": { "version": 6 }, // or { "absent": true } "ttl_s": 7776000 } // 200 OK { "version": 7, "consumed_wcu": 2 } // 409 ConditionFailed carries the current version so the caller can re-read and retry
GET/v1/tables/orders/items/{pk}/{sk}?consistency=strongRead one item
{ "value": { "total": 2499, "status": "paid" }, "version": 7 } // consistency=eventual may be served by any replica and costs half as much
GET/v1/tables/orders/query?pk=customer%234410&sk_from=2026-09-01&limit=50Range over sort keys in one partition key
{ "items": [ /* up to 50, sorted by sk */ ], "next": "eyJzayI6IjIwMjYtMDktMjMi..." }
DELETE/v1/tables/orders/items/{pk}/{sk}Remove an item
// Writes a tombstone; space is reclaimed later by compaction { "condition": { "version": 7 } }
POST/v1/batchUp to 100 gets or 25 writes across partitions
{ "gets": [ { "table": "orders", "pk": "customer#4410", "sk": "2026-10-01#ord_88a" } ] } // Not atomic: the response lists any items that were throttled so the client can retry just those
Why expose capacity units

Charging and throttling in units (one unit per 1 KB written, half a unit per 4 KB read eventually) gives tenants a predictable cost model and gives the system a currency for admission control on each partition.

04

How the pieces fit

05

How the pieces fit

Request path and control planeDiagram
flowchart LR
	C["Client SDK"] --> RR["Request routers"]
	RR --> PM[("Partition map cache")]
	RR --> L["Partition leader"]
	L --> F1["Follower, zone B"]
	L --> F2["Follower, zone C"]
	L --> ENG["LSM engine: WAL, memtable, SSTables"]
	CP["Control plane"] --> META[("Metadata store, Raft")]
	META -. "map updates" .-> PM
	CP --> PL["Placement and split manager"]
	PL -. "move or split" .-> L
	ENG -. "log segments" .-> BK[("Object store backups")]
	L -. "committed changes" .-> CS[["Change stream"]]

The data plane has two layers. Request routers are stateless servers that authenticate the caller, check the table's rate limits, hash the partition key, and look up which partition and which leader own that hash in a cached partition map. Storage nodes host many partition replicas each. Every partition has three replicas in three availability zones forming one Raft group, with one leader that serves strong reads and all writes.

The control plane owns the partition map and never sits on the request path. It stores tables, partitions, hash ranges and replica placement in a small replicated metadata store. A placement manager watches partition sizes, request heat and node health, and decides when to split a partition, move a replica or replace a dead node. Routers subscribe to map changes; if a router sends a request to the wrong node anyway, the node answers with a redirect and the current map version.

Each storage node runs an LSM engine per partition, or one shared engine with partition-prefixed keys. Committed writes also flow to a change stream so other systems can react, and log segments and SSTables are shipped to object storage for backups and point-in-time restore.

Request routers

A few hops of logic: auth, admission control, hash(pk), map lookup, forward. Keeping them stateless means any router can serve any key and they scale with plain load balancing.

Partitions

Each owns a contiguous slice of a 64-bit hash space for one table. Around 10 GB and a few thousand operations a second is the target before a split.

Raft group per partition

Three replicas, one per zone. A write commits when the leader and one follower have it in their logs. Leaders are spread so no node leads more than its share.

LSM engine

Turns random writes into sequential ones. Bloom filters and a block cache keep point reads to about one disk access.

Control plane

Source of truth for the map. Itself a small Raft cluster. If it is down, the data plane keeps serving with the last map; only splits and moves pause.

Change stream and backups

Per-partition ordered change records, retained for 24 hours, plus continuous log archiving which lets an operator rewind a table to a chosen moment within a five-week window.

05

Deep dive: placement, replication and the storage engine

06

Deep dive: placement, replication and the storage engine

Follow one write all the way down

The clearest way to explain this system is to trace put(customer#4410, ...) from the client to the disk, then come back up and ask what happens if each step fails.

Finding the owner

The router hashes the partition key to a 64-bit value. The table's hash space is cut into ranges, one per partition, and the map says which range holds that value and which replica currently leads. This is consistent hashing in spirit, since adding capacity only moves the keys in the ranges being split or moved, but with explicit, centrally managed ranges instead of a ring of random virtual nodes. Explicit ranges let the placement manager split a hot range at its median or move one replica without reshuffling anything else.

Sort keys do not affect placement. All items for customer#4410 live in one partition, ordered by sort key, which is what makes range queries cheap. It is also what makes a giant partition key a problem, which we deal with below.

One strongly consistent writeDiagram
flowchart TD
	R["Router: hash key, find leader"] --> L["Leader: check condition and version"]
	L --> W["Append entry to own Raft log, fsync"]
	W --> P["Send entry to both followers"]
	P --> Q{"One follower acknowledged?"}
	Q -->|yes| A["Commit: apply to memtable"]
	A --> OK["Reply with new version"]
	Q -->|"timeout"| T["Retry, or step down if leadership lost"]
	A -. "later" .-> FL["Memtable full: flush to SSTable"]

Replication with a leader

The leader checks any condition against its current state, assigns the next version, and appends the write to its Raft log. It sends the entry to both followers in parallel. As soon as one follower has written it durably, two of three copies exist in two zones, and the entry is committed. The leader applies it to its in-memory table and replies. The slower follower catches up in the background.

Because a single leader orders every write to a partition, conditional writes and version numbers are trivial: the leader always has the latest value. Strong reads also go to the leader, which must be sure it is still the leader. Rather than run a round of consensus per read, the leader holds a time-bounded lease that followers promise not to undercut by electing someone else. As long as the lease is valid, local reads are linearizable. Eventual reads can go to any replica and may be a few milliseconds behind.

If the leader fails, followers stop hearing heartbeats, and after an election timeout of a second or two one of them, necessarily one with every committed entry, becomes leader. Writes to that partition pause for that window. Clients that timed out mid-write do not know whether it committed, so conditional writes with versions, or idempotency tokens stored with the item, let them retry safely.

The LSM engine on each replica

A committed write lands in a sorted in-memory table, already protected by the Raft log, which doubles as the write-ahead log. When the memtable reaches about 64 MB, it is written to disk as an immutable sorted file, an SSTable, in one sequential pass. Disks are good at sequential writes, so write throughput stays high even with small random keys.

Reads check the memtable, then SSTables from newest to oldest. Each SSTable has a Bloom filter that answers "definitely not here" for almost every file, and a sparse index that points to the right block. With filters and a block cache, a point read usually costs at most one disk read.

Files accumulate, so a background compaction merges them, keeping only the newest version of each key and dropping tombstones that are old enough. Leveled compaction keeps read and space amplification low at the cost of rewriting data several times; size-tiered compaction writes less but leaves more files to search and needs more spare disk. A read-heavy multi-tenant service usually prefers leveled. Compaction must be rate limited so it never starves foreground requests of disk bandwidth.

Splitting and moving partitions

When a partition passes about 10 GB or sustains more traffic than one leader should take, the placement manager splits it. The split point is the median of the hash range for size, or the observed traffic median for heat. Both halves stay on the same three replicas at first, so a split is a metadata change and nearly instant. Afterwards, the manager moves one half's replicas to less loaded nodes by adding a new replica, letting it catch up from a snapshot and the log, then removing an old one. At no point does the Raft group lose its majority.

Hashing spreads different keys well but cannot split one key. If one partition key receives 50,000 writes a second, no split helps. The system can absorb short bursts with per-partition burst credit and by placing hot leaders on lightly loaded nodes, but the real fix is in the data model: add a random suffix to hot keys and read across the suffixes, or put a cache in front for read-heavy keys.

06

Designs that look right and are not

07

Designs that look right and are not

Modulo hashing over node count

hash(key) % N works until you add a node and almost every key moves. Use ranges or consistent hashing so growth moves only a small fraction of data.

Quorum words with leader semantics

Claiming both "W plus R greater than N" and linearizable conditional writes without a leader or consensus is a contradiction. Sloppy quorums and hinted handoff break strict overlap. Pick one model.

Wall clocks for conflict resolution

Last-writer-wins by timestamp silently drops writes when clocks drift. With a leader, versions come from the log. Without one, use version vectors or accept the loss knowingly.

Putting the control plane in the request path

If every request asks the metadata service where a key lives, its outage is everyone's outage. Cache the map in routers and correct it lazily on redirects.

Unthrottled compaction

Letting compaction use all disk bandwidth turns a background task into tail latency spikes. Budget it, and alert on growing compaction debt before writes stall.

Scans as a primary access pattern

Full-table scans across hash partitions touch every node. If an application needs them often, it needs an index or a different store.

07

Choices and their costs

08

Choices and their costs

Chosen
Leader per partition with Raft

Gives linearizable reads, safe conditional writes and simple versioning. A partition pauses writes for a second or two during leader election, and the leader's node carries more load than followers.

Strong and simple to reason about
Alternative
Leaderless quorums, Dynamo style

Any replica accepts writes, so availability through partitions is excellent and there is no election pause. Concurrent writes create siblings that someone must reconcile, and conditional writes need extra machinery.

When availability beats consistency
Chosen
Hash partitioning on the partition key

Spreads load evenly regardless of key patterns, such as ids that increase over time. Range queries are limited to one partition key.

Even load
Alternative
Range partitioning on the whole key

Global ordered scans become possible, as in Bigtable or CockroachDB. Sequential keys such as timestamps pile onto the last range and create a moving hot spot.

If ordered scans matter
Chosen
LSM tree storage engine

High write throughput and good compression. Reads may touch several files and compaction adds background work and write amplification.

Write friendly
Alternative
B-tree storage engine

Predictable reads with one path from root to leaf and in-place updates. Random writes cost more I/O and pages fragment over time.

For read-dominated tables
Chosen
Consistency chosen per read

Callers that need the latest value pay for a leader read; everyone else reads from any replica at lower cost and latency. The API has to make the difference obvious.

Flexible
Rejected
Strong reads only

One model is easier to explain, but every read hits a leader, doubling leader load and wasting follower capacity.

Wastes replicas

08

When things break

09

When things break

🔥
One partition key gets most of a table's traffic

A flash sale writes every order to pk=sale#2026. That partition's leader hits its limit and starts throttling while the rest of the table is idle.

Burst credit and leader placement on quiet nodes buy minutes. Expose per-key heat metrics so the owner can see it, and document key sharding with suffixes. Read-heavy hot keys get a cache in front.
⚡
A leader dies mid-write

The client times out. The write may have committed on a follower, or may not exist anywhere. A naive retry could apply an increment twice.

New leaders always hold every committed entry, so no acknowledged write is lost. Make client retries safe with conditional writes on version, or with an idempotency token stored alongside the item.
🐌
Compaction falls behind

A bulk import writes faster than compaction merges. The number of level zero files grows, reads slow down, and eventually the engine stalls writes to protect itself.

Throttle ingest per tenant before the engine has to, alert on pending compaction bytes, and give compaction a guaranteed share of disk bandwidth rather than whatever is left.
✂️
Splits and moves during peak traffic

The placement manager reacts to heat by splitting and moving partitions just as traffic peaks. Snapshot transfers compete with live requests and make things worse.

Split in place first, since that is metadata only, and defer data movement to quieter periods unless a node is in danger. Rate limit snapshot streaming and never move more than a few replicas per node at once.
🧭
Routers hold a stale map

After a split, some routers still send requests to the old leader for half the range.

Every request carries the router's map version; a node that no longer owns the key answers with a redirect and the new version. Correctness never depends on map freshness, only latency does.
🌐
A whole zone goes dark

A third of replicas vanish, including many leaders.

With one replica per zone, every partition keeps a majority. Elections move leadership to the surviving zones within seconds. Keep enough headroom that two zones can carry full load.

09

Interview notes

10

Interview notes

1

Pick a consistency model and say it out loud. Leader with consensus, or leaderless quorums. Then keep your answers consistent with that choice for the rest of the hour.

2

Trace one write end to end. Router, leader, log, followers, memtable, SSTable. It shows depth faster than listing components.

3

Explain why adding a node moves little data. Contrast modulo hashing with ranges or a ring, with a rough fraction of keys that move.

4

Know the LSM read path. Memtable, newest file first, Bloom filters, block cache. And know what compaction trades: read, write and space amplification.

5

Bring up hot keys before you are asked. Splitting cannot fix a single key. Key suffixing and caching can.

6

Keep the control plane off the hot path. Cached maps with redirects mean metadata outages pause rebalancing, not traffic.

7

Talk about retries and idempotency. Ambiguous timeouts are normal in distributed writes; conditional writes are how clients stay correct.

8

Mention multi-tenancy. Per-table rate limits and capacity units prevent one customer's spike from harming everyone else.

10

Growing the design in stages

12

Growing the design in stages

Stage 1: one machine
An embedded engine behind an API

A single server running an LSM library such as RocksDB, with a WAL and nightly backups. It handles tens of thousands of operations a second and fails completely if the box does.

Stage 2: survive a failure
Replicate the whole dataset

Add two followers with consensus or primary-backup replication and automatic failover. Capacity is still bounded by one machine.

Stage 3: outgrow one machine
Partitions and a router

Split data into hash ranges, each a replication group of its own, and add a metadata service and stateless routers. Rebalancing is still manual.

Stage 4: run it as a service
Automatic splits, placement and tenancy

A placement manager splits and moves partitions based on size and heat, tables get capacity units and rate limits, and the change stream and point-in-time restore arrive.

Stage 5: many regions
Cross-region replication

Replicate tables asynchronously between regions for local reads and disaster recovery, with clear rules for conflicting writes, or offer a slower synchronous mode for tables that need it.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.