Designing a Key-Value Store
Designing a Key-Value Store
A key-value store keeps data as pairs: a unique key, and a value stored under it. It offers two operations, put(key, value) and get(key). Caches, session stores, shopping carts and user settings are all key-value workloads. Designing one that spans many machines is a classic interview question because it brings together almost every earlier topic: partitioning, replication, the CAP trade-off, and efficient storage.
Step 1: agree the requirements
Following the interview framework, start with scope. A typical set:
- Values are small, under about 10 KB.
- The store must hold more data than fits on one machine, and grow by adding machines.
- It must stay available when individual machines fail.
- Reads and writes must be fast, measured at a high percentile, not on average.
- The application can tolerate briefly stale reads in exchange for availability (an assumption to confirm; it decides the CAP choice).
The obvious approach: one big hash table
On one machine, a key-value store is simply a hash table in memory: very fast and simple. It stops working for two reasons: the data outgrows one machine's memory and disk, and one machine is a single point of failure. The design therefore has to spread data across many machines and keep copies of it.
Step 2: partition the data
Each key must have a home machine that any server can work out. Plain hash(key) % N would move almost every key whenever a machine is added or removed, so key-value stores use consistent hashing: machines and keys are placed on the same ring, and each key belongs to the first machine clockwise from it. Adding a machine moves only a small share of the keys, and virtual nodes keep the share even. Try it in the consistent hashing visualization.
Step 3: replicate for availability

Each key is stored on N machines, usually 3: its home on the ring and the next N - 1 machines clockwise. If one fails, two copies remain. In practice the copies are placed in different racks or data centres, so a single power or network failure cannot remove all of them.
Step 4: choose consistency with quorums
With three copies, a write cannot wait forever for all three, and a read cannot always ask all three. Quorums make this tunable with two numbers:
- W: a write succeeds once W copies confirm it.
- R: a read asks R copies and returns the newest value among them.

If R + W > N, every read overlaps every successful write in at least one copy, so it sees the latest value. With N = 3, the settings R = 2 and W = 2 give that guarantee while tolerating one slow or failed copy for both reads and writes. Changing them shifts the trade-off:
- W = 1, R = 3: very fast writes, slow reads.
- W = 3, R = 1: fast reads, but writes fail if any copy is down.
- R + W ≤ N: fastest of all, but reads may be stale. This is the availability side of the CAP theorem.
Step 5: handle failures
- Temporary failures. If a home machine is briefly down, the write goes to the next healthy machine on the ring with a note, a hint, saying where it really belongs. When the home machine returns, the data is handed back. Writes keep succeeding throughout.
- Diverged copies. After failures, copies may differ. Machines compare their data in the background using Merkle trees (trees of hashes), which find the differing keys by exchanging only a few hashes rather than all the data.
- Conflicting writes. If two clients update the same key on different copies during a partition, both versions are kept with version information (such as vector clocks) so the conflict can be detected and resolved, either automatically (for example, latest write wins) or by the application.
- Knowing who is alive. Machines exchange membership and health information with random peers, a gossip protocol, so there is no central registry to fail.
Step 6: store data on each machine

A common storage design on each machine, used by Cassandra and similar systems:
- Write path: append the write to a commit log on disk (so it survives a crash), then put it in a sorted memory table. When the memory table is full, write it to disk as an immutable sorted file. Writes are fast because they are sequential appends.
- Read path: check the memory table first. Otherwise, check each file's in-memory Bloom filter and read only the files that may contain the key.
- Compaction: in the background, merge the sorted files to discard old versions and keep the number of files small.
A real system: Amazon Dynamo
Amazon's Dynamo paper (2007) describes exactly this design, built for services such as the shopping cart that must accept writes at all times. In its own words:
- It "exposes two operations: get() and put()".
- It partitions data with consistent hashing and uses quorums: "Setting R and W such that R + W > N yields a quorum-like system", and "The common (N,R,W) configuration used by several instances of Dynamo is (3,2,2)".
- For temporary failures it uses a "sloppy quorum" with hinted handoff: a replica written to another machine "will have a hint in its metadata that suggests which node was the intended recipient".
- It recovers from permanent failures with "Anti-entropy using Merkle trees", tracks versions with vector clocks, and uses a "Gossip-based membership protocol and failure detection" that "avoids having a centralized registry".
- It measures latency "at the 99.9th percentile of the distribution", not the average.
Dynamo's ideas live on in Apache Cassandra, Riak and Amazon DynamoDB.
Summary of the design
getandputon keys, partitioned by consistent hashing with virtual nodes.- N = 3 copies across failure domains.
- Quorums (R + W > N for consistency, lower for availability).
- Hinted handoff, Merkle-tree repair, version information and gossip for failures.
- Commit log, memory table, sorted files and Bloom filters on each machine.
Implementing it in your project
- Use one, rather than build one. Redis is the usual in-memory key-value store; DynamoDB and Cassandra are distributed key-value stores with replication and partitioning built in. Pick based on durability needs: Redis for caches and fast shared state, DynamoDB or Cassandra for durable data at large scale.
- To learn by building, write a toy in stages: an in-memory hash map with
getandput; then an append-only log file so data survives a restart; then three processes with a consistent hash ring to choose the home of each key; then writes to N = 3 copies with W = 2 and reads with R = 2. Each stage shows why the real systems have the parts they have.
Sources and credibility
Everything above is written from scratch; the quotes come from the source below, attributed where used.
Primary (highest confidence)
- Dynamo: Amazon's Highly Available Key-value Store, DeCandia et al., SOSP 2007. Source of the interface, the quorum rule and (3,2,2) setting, the sloppy quorum, hinted handoff, Merkle trees, gossip and the 99.9th percentile requirement.
- Bloom Filters, Apache Cassandra documentation. Source of the use of Bloom filters on the read path.
Method
- The step-by-step structure follows the key-value store chapter of Alex Xu's System Design Interview, Vol. 1. The explanations are our own.
On copyright: the diagrams on this page were drawn for this article, and the explanations are original. Quoted phrases are short and attributed.