Distributed key-value store¶
At a glance
Priority: P1 · Complexity: 5/5 · Est. time: 4 h · Phase: 5 · Prereqs: Partitioning & consistent hashing, Replication, Consistency models, Consensus: Raft, Databases & storage engines You're done when: you can design both a Dynamo-style (leaderless, AP) and a Raft-based (CP) KV store, explain quorum math, conflict resolution, membership, and the storage engine, and pick one for a given workload.
This is the "build the database" question. It tests whether you understand the distributed-systems primitives underneath every managed service you use: partitioning, replication, quorums, failure detection, conflict resolution, and storage engines.
Problem statement¶
Design a horizontally scalable, highly available key-value store (get(key), put(key, value), delete(key)) serving many internal services — like Amazon Dynamo/DynamoDB, Cassandra, Riak, or etcd/TiKV on the CP side.
Clarifying questions to ask¶
| Question | Why | Assumption |
|---|---|---|
| Consistency requirement: linearizable or eventual? | AP vs CP architecture | Tunable per request; default eventual (shopping cart style) |
| Value size? | Storage engine, network | ≤ 100 KB typical, avg 1 KB |
| Read/write mix? | LSM vs B-tree | Write-heavy (50/50 to 30/70) |
| Scale? | Node count | 100 TB logical, 1 M ops/s |
| Multi-region? | Replication topology | Single region first, multi-region later |
| Range scans / secondary indexes? | Partitioning scheme | Point ops only (hash partitioning) |
| Transactions? | Coordination | Single-key atomicity; conditional put (CAS) |
Functional & non-functional requirements¶
Functional: get/put/delete; conditional put (compare-and-set on version); TTL; per-request consistency level.
Non-functional: p99 < 10 ms for gets within region; 99.99% availability; durability 11 nines-ish via replication + backups; linear horizontal scalability; tolerate node and AZ failures without downtime; online rebalancing.
Back-of-envelope estimation¶
Data: 100 TB logical × RF 3 = 300 TB raw; + ~30% compaction/space amp ≈ 400 TB
Node: ~4 TB usable NVMe/node (keep < 50–60% full for compaction headroom) → ~2 TB effective
→ 400 TB / 2 TB ≈ 200 nodes
Throughput: 1 M ops/s (50% reads) → per node ≈ 5 k client ops/s, but each write hits 3 replicas
→ ~7.5 k replica writes/s/node; a modern LSM node handles 20–50 k/s → headroom OK
Key count: 100 TB / 1 KB ≈ 100 B keys; Bloom filters at 10 bits/key ≈ 125 GB across cluster (~0.6 GB/node) → fits in RAM
Network: 1 M ops × 1 KB × ~3 (replication) ≈ 3 GB/s cluster-wide ≈ 15 MB/s/node — trivial
Virtual nodes: 200 nodes × 256 vnodes = 51,200 token ranges
API design¶
get(key, consistency=ONE|QUORUM|ALL) → (value, version/context)
put(key, value, context?, consistency=..., ttl?) → version
put_if(key, value, expected_version) → ok | conflict # CAS; needs consensus/LWT in AP systems
delete(key) → tombstone write
The context (vector clock or version) returned by get and passed back on put is Dynamo's mechanism for causal tracking — mention it explicitly.
Data model¶
Flat namespace of key → (value, version, ttl, tombstone flag), hashed onto a ring. Internally per node: commit log (WAL), memtable, SSTables with Bloom filters and sparse index; hinted-handoff store; Merkle trees per token range for anti-entropy.
High-level design¶
flowchart LR
CL[Client / smart client] --> CO[Coordinator node]
CO -->|hash key to ring| N1[Replica 1]
CO --> N2[Replica 2]
CO --> N3[Replica 3]
subgraph Node internals
WAL[Commit log] --> MT[Memtable]
MT --> SST[SSTables + Bloom filters]
SST --> CMP[Compaction]
end
N1 -. gossip membership .- N2
N2 -. gossip .- N3
N1 -. Merkle anti-entropy .- N3
Write: coordinator hashes key → preference list of N replicas (next N distinct physical nodes clockwise on the ring, across AZs) → sends to all N, waits for W acks. Read: sends to R replicas (or all, with speculative retry), returns newest version, triggers read-repair for stale replicas.
Deep dives¶
1. Partitioning: consistent hashing with virtual nodes¶
| Scheme | Pros | Cons |
|---|---|---|
| Modulo hashing | Trivial | Adding a node remaps ~all keys |
| Consistent hashing (1 token/node) | Adding a node moves ~1/N keys | Uneven load, hotspots when a node dies (neighbour takes all) |
| Consistent hashing + vnodes (Dynamo/Cassandra) | Even spread, rebuild load shared by many nodes | More metadata, repair across many ranges |
| Fixed partitions (e.g. 4,096) assigned to nodes (Riak, Kafka, DynamoDB-like) | Rebalancing = moving whole partitions; simple ops | Must choose partition count upfront (split later) |
Decision: fixed large number of partitions mapped to nodes by a placement service (or vnodes if leaderless and fully decentralised). Placement must be AZ-aware: replicas of a partition in 3 different AZs.
2. Replication & consistency: leaderless quorums vs Raft groups¶
| Leaderless (Dynamo) | Leader per partition with Raft (TiKV, CockroachDB, etcd) | |
|---|---|---|
| Consistency | Tunable; R+W>N gives overlap but not linearizability (sloppy quorums, concurrent writes) | Linearizable reads/writes (with leader leases or ReadIndex) |
| Availability under partition | Writes accepted on any reachable nodes (sloppy quorum + hinted handoff) | Minority side unavailable |
| Write latency | 1 RTT to W fastest replicas | Leader + majority ack (1 RTT from leader) + client→leader hop |
| Conflicts | Possible → vector clocks / LWW / CRDTs | None (single leader order) |
| Complexity | Anti-entropy, read repair, tombstones | Consensus, leader election, membership changes |
Quorum math to say out loud: N=3, W=2, R=2 → tolerate 1 replica down for both reads and writes; W=1,R=3 for write-heavy; W=3 kills write availability on any failure. Decision: depends on the workload — for a cart/session/profile store, leaderless AP with LWW or CRDTs; for metadata, config, locks, inventory counts → Raft-based CP. A strong answer offers both and picks per requirement.
3. Conflict resolution¶
| Strategy | Use when | Risk |
|---|---|---|
| Last-write-wins (timestamp) | Idempotent overwrites, caches | Silent data loss under clock skew |
| Vector clocks + client merge (Dynamo cart) | Application can merge | Clients must handle siblings; clock pruning |
| CRDTs (counters, sets, maps) | Commutative data types | Limited data types, metadata growth |
| Conditional writes via Paxos/Raft (LWT) | Must-not-lose updates | Latency (~4 RTT in Cassandra LWT) |
Decision: LWW default with hybrid logical clocks, CRDT types for counters/sets, and LWT/CAS for the few operations that need it.
4. Storage engine: LSM vs B-tree¶
| LSM-tree (RocksDB, Cassandra) | B-tree (InnoDB, BoltDB, WiredTiger default) | |
|---|---|---|
| Writes | Sequential, fast; high ingest | Random in-place updates; WAL + page writes |
| Reads | May check multiple SSTables (Bloom filters help) | One tree traversal |
| Amplification | Write amp from compaction (10–30x leveled), space amp lower with leveled | Write amp from page rewrites; fragmentation |
| Tuning | Compaction strategy (size-tiered vs leveled vs time-window) | Page size, fill factor |
Decision: LSM (RocksDB) for a write-heavy KV; leveled compaction for read-heavy partitions, time-window compaction for TTL-heavy data. Tombstones + TTL need gc_grace longer than max repair interval or deleted data resurrects — a classic production bug.
Scaling & bottlenecks¶
- Hot keys/partitions: detect via per-partition metrics; split partitions; add a cache tier; for extreme hot keys, application-level key salting (append suffix, read all shards).
- Rebalancing: stream partitions to new nodes with throttling (bandwidth caps) so client latency isn't hurt; bootstrapping a 2 TB node at 200 MB/s ≈ 3 h.
- Compaction debt: under sustained writes, compaction falls behind → read amp grows; monitor pending compactions and throttle writes (backpressure) before latency collapses.
- Large partitions / wide rows: cap value and partition sizes.
Failure modes & reliability¶
| Failure | Mechanism |
|---|---|
| Node temporarily down | Sloppy quorum + hinted handoff (AP) / Raft re-election (CP, ~1–10 s) |
| Node permanently lost | Re-replicate from other replicas; vnodes spread rebuild across many nodes |
| Divergent replicas | Read repair + periodic anti-entropy via Merkle trees |
| Failure detection | Gossip + phi-accrual detector (adaptive to network jitter) |
| Split brain (CP) | Majority quorum + leader leases with fencing |
| Correlated failure (AZ) | AZ-aware placement; RF=3 across 3 AZs |
| Operator error / corruption | Snapshots to object storage, PITR, checksums per block |
Jepsen analyses are the reality check: many stores advertising strong consistency have failed under partitions — quote that you'd test with fault injection.
Security & multi-tenancy¶
Tenant isolation via namespaces/tables with per-tenant quotas (ops/s, storage) enforced at the coordinator (rate limiting); noisy-neighbour protection via request scheduling (per-tenant fair queues, as DynamoDB describes in its 2022 USENIX paper). Encryption at rest (per-tenant keys for BYOK), TLS/mTLS between nodes, authn/z per namespace, audit logs for admin operations.
How the design changes at 10x / in an AI-era variant¶
10x (1 PB, 10 M ops/s, ~2,000 nodes): gossip overhead and membership churn become real → move to a central placement/metadata service (itself Raft-backed) with nodes reporting heartbeats; automated partition splitting; multi-region with async replication and per-key home regions or CRDTs.
AI-era variant: KV stores now back agent memory, session state, feature stores and prompt/KV caches. Implications: (1) vector values — adding ANN indexes turns the KV into a vector DB (different access pattern: top-k similarity, index build cost); (2) LLM inference prefix/KV-cache offloading uses tiered KV (GPU HBM → CPU RAM → SSD) with very large values (MBs) → value size assumptions change, favour blob-style storage with a small index; (3) agent checkpoints (LangGraph-style) are write-heavy, small, per-thread keys — a perfect LSM workload with TTLs.
What a Staff-level answer adds (vs senior)¶
- Presents AP and CP designs as a choice driven by the business invariant, and explains precisely why R+W>N is not linearizability.
- Talks about operability: compaction debt, repair schedules, tombstone resurrection, rebalancing throttles, upgrade procedures.
- Brings in testing: Jepsen-style fault injection, deterministic simulation testing (FoundationDB, TigerBeetle).
- Knows when not to build: a managed service (DynamoDB, Cosmos DB, Spanner) plus clear SLOs usually beats a bespoke store; justify build only for unique cost/latency/control needs.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| Dynamo: Amazon's Highly Available Key-value Store (SOSP 2007) | paper | The source of the leaderless design vocabulary | advanced | free |
| Amazon DynamoDB: A Scalable, Predictably Performant, Fully Managed NoSQL Database (USENIX ATC '22) |
paper | How the managed service diverged from Dynamo (Multi-Paxos, admission control) | advanced | free |
| Designing Data-Intensive Applications, 2nd ed. | book | Chapters on replication, partitioning, storage engines | advanced | paid |
| Raft paper & visualisation | docs | CP alternative; read with the interactive demo | advanced | free |
| Jepsen analyses |
article | What actually breaks under partitions | advanced | free |
| Patterns of Distributed Systems (Unmesh Joshi) | article | Named patterns: WAL, HLC, gossip, lease, quorum | intermediate | free |
| Hello Interview — Distributed cache | article | Interview-paced sibling problem | intermediate | free |
| Alex Xu — System Design Interview Vol 1, ch. 6 | book | Standard interview framing of this problem | intermediate | paid |
Follow-up questions¶
L2 — Apply¶
Q1. N=3, W=2, R=2. One replica is down and another is slow (200 ms). What are read and write latencies?
Answer
Only two replicas are reachable, and both are needed for W=2 and R=2, so every operation waits for the slow one: ~200 ms. Mitigations: sloppy quorum (write to a healthy fallback node with a hint), speculative retries, or temporarily lowering to W=1/R=1 for tolerant requests. Illustrates why tail latency in quorum systems is governed by the k-th fastest replica.
Q2. You delete a key and it reappears two weeks later. Explain and fix.
Answer
A replica was down (or missed the delete) longer than gc_grace_seconds; the tombstone was compacted away on other replicas, then anti-entropy/read-repair propagated the old value back from the stale replica. Fix: run full repairs more often than gc_grace (e.g. weekly repair, 10-day grace), never bring back a node down longer than grace without wiping it, and alert on repair lag.
Q3. A new node joins a 200-node cluster with vnodes. How much data moves and from where?
Answer
Roughly 1/201 of total data ≈ 400 TB / 201 ≈ 2 TB, streamed from many existing nodes (each vnode range is taken from a different neighbour), so each source sends only ~10 GB. At a throttle of 200 MB/s inbound on the new node, ~3 h. Without vnodes, a single neighbour would source it all, overloading that node.
L3 — Design & trade-offs¶
Q4. A team needs a KV store for inventory counts that must never oversell. Leaderless or Raft? Defend.
Answer
Raft (or Paxos) per partition. The invariant (count ≥ 0) needs linearizable conditional updates; leaderless LWW can lose decrements under concurrency, and sloppy quorums allow divergent writes. Alternatives: CRDT counters can't enforce a lower bound without coordination (escrow/reservation schemes can, by pre-allocating quota per replica). Cost: minority partitions become unavailable and writes pay leader RTT — acceptable for inventory.
Q5. LSM or B-tree for a read-heavy (95% reads), point-lookup workload with small values?
Answer
B-tree is a strong fit: single traversal per read, predictable latency, no compaction stalls. An LSM with leveled compaction, large block cache, and Bloom filters can also deliver ~1 disk read per lookup and better space efficiency with compression. If data fits mostly in RAM, either works; choose by ops familiarity. Mention write amplification only matters little at 5% writes.
Q6. How do you detect failures without false positives in a 200-node cluster across AZs?
Answer
Gossip-based heartbeats with a phi-accrual failure detector (suspicion level adapts to observed inter-arrival distribution) rather than fixed timeouts; require indirect probes (SWIM-style: ask k other nodes to ping the suspect) before marking down. Separate 'down for routing' (fast, reversible) from 'removed from ring' (slow, operator or long timeout) to avoid rebalancing storms on transient blips.
L4 — Staff-level ambiguity¶
Q7. Your org runs a self-managed Cassandra fleet (600 nodes, 5 teams). Leadership asks whether to move to a managed service. How do you decide and drive it?
Answer
Build a TCO including people (on-call, upgrades, repairs — often 3–6 FTE), hardware, incidents; compare to managed pricing at current and 2x load, including egress and provisioned vs on-demand. Evaluate fit: data model compatibility (Keyspaces/Astra/Scylla Cloud vs DynamoDB rewrite), latency, features (LWT, TTL, counters), lock-in. Pilot with one team's workload using dual writes and shadow reads. Present an ADR with options, cost curves, risks and a phased migration; get buy-in from all 5 teams by addressing their specific pain points.
Q8. An incident: a network partition caused two coordinators to accept conflicting writes and customers saw data loss under LWW. What changes?
Answer
Short-term: identify affected keys from logs/hints, restore from siblings/backup, notify customers. Design: classify data by conflict tolerance — move must-not-lose data to CP storage or CAS/LWT paths; for mergeable data adopt CRDTs; replace wall-clock LWW with HLC to reduce skew damage; add clock-skew monitoring. Process: Jepsen-style partition tests in CI for the storage layer; document consistency guarantees per API so product teams choose knowingly.
Q9. Agent-memory and LangGraph checkpoint traffic now dominates your KV platform. What do you change?
Answer
Profile the workload: small frequent writes per thread, reads of latest checkpoint, long tail of cold threads, TTLs. Tune: dedicated tables with time-window compaction and TTL, tiered storage for cold checkpoints (object storage), per-tenant quotas to protect other workloads, and compression of large JSON states. Consider a purpose-specific API (checkpoint store) rather than raw KV so you can change storage later. Add cost attribution per agent/team.
Checklist¶
- I can explain consistent hashing with vnodes vs fixed partitions
- I can do quorum math and explain why R+W>N ≠ linearizable
- I can compare LWW, vector clocks, CRDTs, and LWT
- I can explain LSM compaction, Bloom filters, and tombstone resurrection
- I answered all L3 questions out loud in < 3 min each