Storage & Data
Design Key-Value Store
01
Scope and targets
Scope and targets
get,putanddeleteon 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.
- 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.
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
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.
| Quantity | Working | Result |
|---|---|---|
| Peak reads | 4M x 70% | 2.8M / s |
| Peak writes | 4M x 30% | 1.2M / s |
| Replica write operations | 1.2M x 3 copies | 3.6M / s |
| Logical data | 40B items x 1.5 KB | 60 TB |
| Stored bytes | 60 TB x 3 replicas x 1.2 LSM space overhead | about 216 TB |
| Partitions | 60 TB / 10 GB target size | about 6,000, so 18,000 replicas |
| Nodes for storage | 216 TB / (4 TB NVMe x 60% fill target) | about 90 |
| Nodes for throughput | (2.8M + 3.6M) / about 80K ops per node | about 80 |
| Provisioned fleet | max of the two, plus a zone of headroom | about 130 nodes, 140 replicas each |
| Write bandwidth into the WAL | 3.6M x 1.6 KB | about 5.8 GB / s cluster wide |
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.
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
Client interface
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
How the pieces fit
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.
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.
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.
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.
Turns random writes into sequential ones. Bloom filters and a block cache keep point reads to about one disk access.
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.
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
Deep dive: placement, replication and the storage engine
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.
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
Designs that look right and are not
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.
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.
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.
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.
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.
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
Choices and their costs
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 aboutAny 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 consistencySpreads load evenly regardless of key patterns, such as ids that increase over time. Range queries are limited to one partition key.
Even loadGlobal 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 matterHigh write throughput and good compression. Reads may touch several files and compaction adds background work and write amplification.
Write friendlyPredictable 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 tablesCallers 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.
FlexibleOne model is easier to explain, but every read hits a leader, doubling leader load and wasting follower capacity.
Wastes replicas08
When things break
When things break
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.
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.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.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.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 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
Interview notes
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.
Trace one write end to end. Router, leader, log, followers, memtable, SSTable. It shows depth faster than listing components.
Explain why adding a node moves little data. Contrast modulo hashing with ranges or a ring, with a rough fraction of keys that move.
Know the LSM read path. Memtable, newest file first, Bloom filters, block cache. And know what compaction trades: read, write and space amplification.
Bring up hot keys before you are asked. Splitting cannot fix a single key. Key suffixing and caching can.
Keep the control plane off the hot path. Cached maps with redirects mean metadata outages pause rebalancing, not traffic.
Talk about retries and idempotency. Ambiguous timeouts are normal in distributed writes; conditional writes are how clients stay correct.
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
Growing the design in stages
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.
Add two followers with consensus or primary-backup replication and automatic failover. Capacity is still bounded by one machine.
Split data into hash ranges, each a replication group of its own, and add a metadata service and stateless routers. Rebalancing is still manual.
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.
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
Further watching and reading
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.
