Problem library

Track 1 · Foundations

hard

Distributed Key-Value Store

Build the storage layer the other problems assume exists. Consistent hashing for placement, quorums for consistency, and an explicit answer to what happens during a partition.

4 parts · 2 workloads
consistent hashingreplicationquorumCAP

Suggested architecture

Scenario

Every write is multiplied by the replication factor before it reaches disk. The ring's load is reads + R × writes, spread over N nodes — and N is the only knob that moves the per-node number.

Gets per second, each served by one replica.

Puts per second, each written to every replica.

Copies of every key. Durability up, write cost up, by the same factor.

Physical nodes. Load spreads evenly only if virtual nodes are doing their job.

Open in playground →This diagram is a playground design: the sliders write onto it and the same engine judges it. Open it to change anything, run a spike, or price it.
Entering
~60K req/s
Reads
7.8 ms
Writes
7.8 ms
Busiest
Coordinator 50%
synchronousasynchronousfallback / miss path
get / putgetput × N replicasreplica down
Components

Click a component for its role, common technology choices and tradeoffs, and what it is carrying at this scale. Hover a connection to see what flows along it. Drag to rearrange — layout changes are local and reset on reload.

Every figure here is a rough estimate from simple capacity arithmetic, not a benchmark. Each part carries its own assumption about what one copy can do — real numbers depend on your hardware, payloads and access pattern. The point is which component moves first as you turn the dials, not the digits themselves.

In plain words

A key-value store is the simplest database there is — put(key, value) and get(key) — spread across many machines so it survives any one of them dying. The hard part is not storing bytes; it is deciding what a reader is promised when the copies disagree. This problem is where "consistency", "replication" and "quorum" stop being vocabulary and become dials you set.

Four decisions, in order:

  1. Placement — which node owns a key?
  2. Replication — how many copies, and where?
  3. Consistency — what does a read guarantee?
  4. Failure — what happens when a replica or the network is gone?

1. Placement: a ring, not a modulo

The naive rule, hash(key) % N, is a trap: go from 10 nodes to 11 and about 90% of keys move to a different node. Consistent hashing puts nodes and keys on a ring; a key belongs to the first node clockwise from its hash. Adding a node moves only about 1/N of keys, all from one neighbour.

The refinement that makes it work is virtual nodes: each physical node sits at 100–200 points on the ring. Without them, random placement leaves some nodes with huge arcs and others with slivers, and a node leaving dumps its whole load on a single neighbour.

2. Replication: the next N nodes clockwise

Store each key on the next N distinct physical nodes clockwise (distinct matters — with virtual nodes the next three ring positions could be the same machine). N = 3 is the usual choice.

put(k, v)ClientCoordinatorany nodeNode Acopy 1Node Bcopy 2Node Ccopy 3
A write goes to all N replicas. How many must answer before the client hears 'done' is the W dial.

3. Consistency: two dials, W and R

A quorum is the minimum number of copies that must agree before an operation counts as done. The trick is to make it a setting:

  • W — replicas that must acknowledge a write.
  • R — replicas that must respond to a read.
N = 3Write waits forRead asksYou get
W=1, R=111Fastest. Eventually consistent: a read right after a write may return the old value
W=2, R=222The usual default. Fresh reads, survives one node down for both reads and writes
W=3, R=131Fast reads, but one node down blocks all writes
W=1, R=313Fast writes; every read is as slow as the slowest node

This is the CAP trade-off turned into two numbers the caller chooses per request, instead of a fixed property of the system.

A quorum write and read, sketchedPython
def put(key, value, W=2):
    version = clock.next(key)                     # vector clock or timestamp
    acks = 0
    for node in ring.replicas(key, N=3):          # send to all three
        if node.write(key, value, version): acks += 1
        if acks >= W: return True                 # answer as soon as W have it
    raise WriteFailed                             # fewer than W alive: refuse

def get(key, R=2):
    answers = [n.read(key) for n in ring.replicas(key, N=3)][:R]  # wait for R
    newest = max(answers, key=lambda a: a.version)
    for a in answers:
        if a.version < newest.version: a.node.write(key, newest.value, newest.version)  # read repair
    return newest.value

Conflicts: when two copies both think they are right

With W=2 and two clients writing the same key at the same moment through different coordinators, two replicas can hold different values with no "newer" between them. Three ways to settle it:

Last write wins
by timestamp

Keep the later timestamp. Simple — and clock skew (two machines' clocks disagreeing) silently discards a real write. Fine for caches and sessions.

Vector clocks
correct, heavier

A version counter per replica, so the store can tell "newer" from "written at the same time". Truly concurrent versions are both kept and handed to the client to merge. Correct, but every caller needs a merge rule.

CRDTs
when the value fits

Conflict-free replicated data types — counters, sets, maps that merge the same whatever the order. No client-side merging. Not general.

4. Failure handling

CoordinatorNode BNode Dwrite k=vB is downwrite k=v (hint: this belongs to B)hinted handoff — D holds it for Back…B comes back…replay hinted writes for Bcaught up
Hinted handoff keeps writes flowing during a short outage; the hint is replayed when the node returns.
  • Hinted handoff (in the diagram): a healthy node accepts writes meant for a down node and replays them when it recovers.
  • Anti-entropy with Merkle trees: replicas compare hash trees of their key ranges (a hash per chunk, then a hash of the hashes, up to one root) to find where they differ without streaming the whole dataset, then repair.
  • Gossip: nodes swap membership and health with a few random peers every second. Failure detection with no central monitor and no single point of failure.

Where this design breaks

  • Hot keys. The ring spreads keys evenly, not traffic. One viral key still lands on N nodes and can saturate them. Cache it in front.
  • Large values. The design assumes small values. Multi-megabyte values make replication bandwidth, not storage, the binding constraint.
  • Range scans. Hash placement destroys key order, so "all keys between X and Y" means asking every node. If you need ranges, you want range partitioning and its hot-spot problem instead.
  • Cross-key transactions. Two keys can live on disjoint node sets, so there is no atomic multi-key write without a coordination protocol this design does not have.
  • Clock skew. Any last-write-wins scheme is only as trustworthy as NTP, the protocol that keeps clocks in sync — good to milliseconds, not microseconds.

Take this with you

  • The one idea: placement, replication, consistency and failure are four separate decisions. Make each one on purpose.
  • In an interview, draw the ring, say "N copies, W to write, R to read, R + W > N for fresh reads", and name one way to settle conflicts.
  • At work, check what W and R your store actually runs with. The default is often the fast one, and "we read our own write back" is not guaranteed there.