#Design a Distributed Key-Value Store (DynamoDB Scale)
#1. Problem Statement & Clarifications
A distributed key-value store provides a simple put(key, value) / get(key) interface distributed across multiple machines for scalability, fault tolerance, and low latency. Think DynamoDB, Cassandra, or Riak. The core challenge is balancing consistency, availability, and partition tolerance (CAP theorem) while maintaining sub-millisecond performance.
#Functional Requirements
- Put(key, value) β Store a key-value pair (value up to 1MB)
- Get(key) β Retrieve value by key with configurable consistency
- Delete(key) β Remove a key-value pair
- TTL support β Keys can have configurable expiration
- Tunable consistency β Client chooses strong or eventual consistency per request
#Non-Functional Requirements
| Requirement | Target |
|---|---|
| Read Latency | P99 < 5ms (eventual), P99 < 10ms (strong) |
| Write Latency | P99 < 10ms |
| Throughput | 1M+ ops/sec per cluster |
| Availability | 99.99% β prefer AP over CP |
| Durability | No data loss after acknowledged write |
| Scalability | Linear horizontal scaling (add nodes β proportional throughput) |
#Out of Scope
- Range queries / secondary indexes (this is pure KV)
- Transactions (multi-key atomicity)
- SQL query layer
- Full-text search
#Assumptions
- Average key: 32 bytes; average value: 1KB; max value: 1MB
- Read-heavy workload (80% reads, 20% writes)
- Keys distributed uniformly (no extreme hot keys)
- Cluster size: 100β1000 nodes
- Data center aware: multi-AZ deployment
#2. Back-of-Envelope Estimation
#Traffic Estimates
Total ops/sec: 1M (800K reads + 200K writes)
Reads/sec (peak 3x): 2.4M
Writes/sec (peak 3x): 600K
Per-node (100 nodes): 10K ops/sec avg per node#Storage Estimates
Record size avg: key (32B) + value (1KB) + metadata (100B) β 1.1KB
New writes/day: 200K/sec Γ 86400 = 17.3B writes/day
Unique keys (5yr): ~100B (with overwrites and deletes)
Active dataset: 10B keys Γ 1.1KB = 11TB
With 3x replication: 33TB across cluster
Per-node (100 nodes): 330GB per node#Bandwidth
Read bandwidth: 800K Γ 1.1KB = 880 MB/s cluster-wide
Write bandwidth: 200K Γ 1.1KB Γ 3 (replicas) = 660 MB/s
Per-node: ~15 MB/s (well within NVMe SSD capacity)#Cache Estimates
Hot keys (top 20%): 2B keys Γ 1.1KB = 2.2TB
Per-node in-memory: 22GB (feasible with 64GB+ RAM nodes)
Memory-first architecture: keep hot data in RAM, cold on SSD#3. API Design
#Put (Write)
PUT /api/v1/kv/{key}
Content-Type: application/octet-stream
X-Consistency: quorum // one | quorum | all
X-TTL: 3600 // optional, seconds
Body: <raw value bytes>
Response 200:
{
"key": "user:12345:profile",
"version": 1716984000123456, // vector clock / timestamp
"size": 1024,
"replicas_ack": 2 // how many replicas acknowledged
}#Get (Read)
GET /api/v1/kv/{key}
X-Consistency: quorum // one | quorum | all
Response 200:
Content-Type: application/octet-stream
X-Version: 1716984000123456
X-Replicas-Read: 2
Body: <raw value bytes>
Response 404: { "error": "key_not_found" }#Delete
DELETE /api/v1/kv/{key}
X-Consistency: quorum
Response 204: No Content#Batch Operations
POST /api/v1/kv/batch
{
"operations": [
{ "op": "get", "key": "user:123:profile" },
{ "op": "put", "key": "user:456:session", "value": "base64...", "ttl": 3600 },
{ "op": "delete", "key": "temp:789" }
]
}#4. Data Model
#Storage Engine (LSM-Tree based β per node)
Write path:
1. Write to WAL (Write-Ahead Log) on disk β durability
2. Insert into MemTable (in-memory sorted structure) β fast writes
3. When MemTable full β flush to SSTable on disk β persistent
4. Background compaction merges SSTables β read optimization
Read path:
1. Check MemTable (in-memory) β fastest
2. Check Bloom filter for each SSTable β skip irrelevant files
3. Binary search in SSTable index β find key
4. Read value from SSTable data block β return#Data Record Format
ββββββββββββ¬βββββββββββββ¬βββββββββββ¬ββββββββββββ¬βββββββββββ¬ββββββββ
β Key Size β Value Size β Key β Value β Timestampβ Flags β
β (4B) β (4B) β (var) β (var) β (8B) β (1B) β
ββββββββββββ΄βββββββββββββ΄βββββββββββ΄ββββββββββββ΄βββββββββββ΄ββββββββ
Flags: deleted (tombstone), compressed, TTL-expired#Partition Map (Consistent Hashing Ring)
Hash ring with virtual nodes:
- Each physical node β 150-200 virtual nodes on the ring
- Key β hash(key) β find next N nodes clockwise β those are replicas
- N = replication factor (typically 3)
Ring metadata stored in coordinator / gossip protocol#Access Patterns
| Query | Path | Mechanism |
|---|---|---|
| Put key | Coordinator β N replica nodes (quorum write) | Consistent hashing ring lookup |
| Get key | Coordinator β R replica nodes (quorum read) | Read from R replicas, return latest version |
| Delete key | Write tombstone marker to N replicas | Tombstone garbage collected after grace period |
| TTL expiry | Background compaction removes expired records | Checked during compaction + read path |
#5. High-Level Design (HLD) & Scale Evolution
#Stage 1: MVP / Single Node (10K ops/sec)
- Architecture: Single-server KV store with LSM-tree. WAL + MemTable + SSTables.
- No distribution: All data on one machine.
- Bottleneck: Single machine storage/throughput limit. No fault tolerance.
#Stage 2: Growth Scale (100K ops/sec, 10 nodes)
- Key Changes:
- Consistent hashing to partition keys across nodes.
- Replication factor = 3 for fault tolerance.
- Quorum reads/writes (W=2, R=2, N=3) for tunable consistency.
- Gossip protocol for failure detection and membership.
- Bottleneck: Hot keys cause uneven load. Need virtual nodes + request routing.
#Stage 3: DynamoDB Scale (1M+ ops/sec, 100+ nodes)
#Architecture Diagram
ββββββββββββββββββββββββββββ
β Client SDK β
β (partition-aware routing) β
ββββββββββββββ¬ββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Node 1 β β Node 2 β β Node N β
β β β β β β
β βββββββββββββ β β βββββββββββββ β β βββββββββββββ β
β βCoordinatorβ β β βCoordinatorβ β β βCoordinatorβ β
β βββββββ¬ββββββ β β βββββββ¬ββββββ β β βββββββ¬ββββββ β
β β β β β β β β β
β βββββββΌββββββ β β βββββββΌββββββ β β βββββββΌββββββ β
β β Storage β β β β Storage β β β β Storage β β
β β Engine β β β β Engine β β β β Engine β β
β β (LSM-Tree) β β β β (LSM-Tree) β β β β (LSM-Tree) β β
β β β β β β β β β β β β
β β MemTable β β β β MemTable β β β β MemTable β β
β β WAL β β β β WAL β β β β WAL β β
β β SSTables β β β β SSTables β β β β SSTables β β
β βββββββββββββ β β βββββββββββββ β β βββββββββββββ β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β β
ββββββββββββββββββββΌβββββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ
β Gossip Protocol β
β (Membership + Failure β
β Detection) β
βββββββββββββββββββββββββββ#Component Breakdown
| Component | Responsibility | Tech Choice |
|---|---|---|
| Client SDK | Partition-aware routing; retry logic; consistency selection | Language-specific SDK |
| Coordinator | Request routing; quorum management; conflict resolution | Every node is a coordinator |
| Storage Engine | LSM-tree: WAL + MemTable + SSTables; compaction | Custom (RocksDB-like) |
| Consistent Hashing Ring | Key-to-node mapping; virtual nodes; rebalancing | In-process ring state |
| Gossip Protocol | Membership; failure detection; ring state propagation | Swim / Memberlist |
| Replication Manager | Async/sync replica writes; read repair; hinted handoff | Per-node module |
#Data Flow
Write Path (Quorum Write W=2, N=3):
Client β Coordinator (any node)
β 1. hash(key) β find N=3 replica nodes on consistent hash ring
β 2. Send write request to all 3 replicas in parallel
β 3. Each replica: WAL append β MemTable insert β ACK
β 4. Coordinator waits for W=2 ACKs β return success to client
β 5. Third replica ACKs asynchronously (or via hinted handoff if down)Read Path (Quorum Read R=2, N=3):
Client β Coordinator
β 1. hash(key) β find N=3 replica nodes
β 2. Send read request to all 3 replicas in parallel
β 3. Wait for R=2 responses
β 4. Compare versions β return latest value
β 5. If versions differ β trigger read repair on stale replica#6. Deep Dive β Core Components
#Consistent Hashing β Detailed Design
Why not modular hashing (key % N)? When adding/removing nodes, modular hashing redistributes almost all keys. Consistent hashing redistributes only K/N keys (K = total keys, N = nodes).
Virtual Nodes:
Physical Node A β VNode_A1, VNode_A2, ..., VNode_A150
Physical Node B β VNode_B1, VNode_B2, ..., VNode_B150
Benefits:
- Even key distribution (more points on ring)
- Heterogeneous hardware (stronger node β more vnodes)
- Smoother rebalancing when nodes join/leave#Replication & Consistency β Detailed Design
Quorum System: W + R > N guarantees overlap
N = 3 (replication factor)
Strong consistency: W=2, R=2 (always reads at least one up-to-date replica)
High availability: W=1, R=1 (faster but may read stale data)
Strongest: W=3, R=1 or W=1, R=3 (one side sees all replicas)Conflict Resolution (when W + R β€ N or partitioned):
Strategy: Last-Write-Wins (LWW) with vector clocks
Vector Clock: [(node_A, 3), (node_B, 1), (node_C, 2)]
- Each node increments its own counter on write
- On read: compare vector clocks
- One dominates β choose it (clear winner)
- Concurrent (neither dominates) β application resolves OR LWW by timestamp#Storage Engine (LSM-Tree) β Detailed Design
Write: O(1) amortized
Read: O(log N) with Bloom filters
MemTable (in-memory, sorted β Red-Black Tree or SkipList)
ββ flush when > 64MB
ββ SSTable (immutable sorted file on disk)
ββ Bloom filter per SSTable (false positive: 1%)
ββ Sparse index (one entry per 64KB block)
Compaction (background):
Size-tiered: merge SSTables of similar size β reduce read amplification
Leveled: maintain sorted levels β better read performance, more write I/O#Scaling Strategy
| Component | Strategy |
|---|---|
| Nodes | Add nodes β automatic rebalancing via consistent hash ring |
| Storage | LSM-tree per node; NVMe SSDs; tiered compaction |
| Hot keys | Client-side caching; request routing with read replicas |
| Replication | Configurable N per keyspace; rack/AZ-aware placement |
| Failure | Gossip detection; hinted handoff; Merkle tree anti-entropy |
#Caching Strategy
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 1: Client SDK Cache β
β β’ LRU cache for frequently accessed keys β
β β’ TTL-based invalidation; cache size configurable β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 2: MemTable (Per-Node In-Memory) β
β β’ Most recent writes always in memory β
β β’ 64MB per MemTable; multiple active MemTables β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 3: OS Page Cache β
β β’ SSTables read via mmap; hot blocks cached by OS β
β β’ Effectively unlimited with enough RAM β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 4: Bloom Filters (Per-SSTable) β
β β’ Avoid reading SSTables that don't contain the key β
β β’ 10 bits/key β 1% false positive rate β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ#Consistency Models
| Data | Model | Rationale |
|---|---|---|
| Writes (W=2, N=3) | Strong (within quorum) | W+R>N guarantees read sees latest write |
| Writes (W=1, N=3) | Eventually consistent | Fast writes; stale reads possible for up to seconds |
| Membership (gossip) | Eventually consistent (< 10s) | Ring state converges via gossip protocol |
| Anti-entropy repair | Eventually consistent (< 1h) | Merkle tree comparison runs periodically |
#7. Low-Level Design β Core Functionality
#Key Algorithms
1. Consistent Hashing with Virtual Nodes
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, vnodes_per_node=150):
self.vnodes = vnodes_per_node
self.ring = [] # sorted list of (hash, node_id)
self.ring_hashes = [] # just hashes for bisect
self.nodes = {} # node_id β node_info
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node_id, node_info):
self.nodes[node_id] = node_info
for i in range(self.vnodes):
vnode_key = f"{node_id}:vnode:{i}"
h = self._hash(vnode_key)
idx = bisect.bisect(self.ring_hashes, h)
self.ring_hashes.insert(idx, h)
self.ring.insert(idx, (h, node_id))
def get_nodes(self, key, n=3):
"""Get N unique physical nodes for a key."""
h = self._hash(key)
idx = bisect.bisect(self.ring_hashes, h) % len(self.ring)
result = []
seen = set()
while len(result) < n:
_, node_id = self.ring[idx % len(self.ring)]
if node_id not in seen:
result.append(node_id)
seen.add(node_id)
idx += 1
return result2. Quorum Read/Write
def quorum_write(key, value, n=3, w=2):
"""Write to N replicas, wait for W acknowledgments."""
nodes = ring.get_nodes(key, n)
timestamp = time.time_ns()
version = VectorClock.increment(local_node_id)
# Send writes in parallel
futures = []
for node in nodes:
f = async_write(node, key, value, timestamp, version)
futures.append(f)
# Wait for W acks
acks = 0
for f in as_completed(futures):
if f.result().success:
acks += 1
if acks >= w:
return WriteResult(success=True, version=version)
# Failed to get quorum β write fails
raise WriteQuorumNotMet(acks_received=acks, required=w)
def quorum_read(key, n=3, r=2):
"""Read from N replicas, wait for R responses, return latest."""
nodes = ring.get_nodes(key, n)
futures = [async_read(node, key) for node in nodes]
responses = []
for f in as_completed(futures):
resp = f.result()
if resp.found:
responses.append(resp)
if len(responses) >= r:
break
if not responses:
raise KeyNotFound(key)
# Return the latest version
latest = max(responses, key=lambda r: r.timestamp)
# Read repair: update stale replicas asynchronously
for resp in responses:
if resp.timestamp < latest.timestamp:
async_repair(resp.node, key, latest.value, latest.version)
return latest.value3. Merkle Tree for Anti-Entropy
def build_merkle_tree(key_range, node):
"""Build Merkle tree over a key range for anti-entropy sync."""
keys = node.get_keys_in_range(key_range)
leaves = [hash(f"{k}:{node.get_version(k)}") for k in sorted(keys)]
# Build tree bottom-up
tree = [leaves]
while len(tree[-1]) > 1:
level = tree[-1]
parent = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i+1] if i+1 < len(level) else left
parent.append(hash(f"{left}:{right}"))
tree.append(parent)
return tree # root = tree[-1][0]
def anti_entropy_sync(node_a, node_b, key_range):
"""Compare Merkle trees to find divergent keys."""
tree_a = build_merkle_tree(key_range, node_a)
tree_b = build_merkle_tree(key_range, node_b)
if tree_a[-1] == tree_b[-1]: # roots match
return # all keys in sync
# Traverse tree to find divergent leaves β sync only those keys
divergent_keys = find_divergent_leaves(tree_a, tree_b)
for key in divergent_keys:
sync_key(node_a, node_b, key) # copy latest version#Design Patterns Used
| Pattern | Where | Why |
|---|---|---|
| Consistent Hashing | Key-to-node partitioning | Even distribution; minimal redistribution on node changes |
| Quorum (Sloppy) | Read/write consistency | Tunable consistency; W+R>N for strong reads |
| Hinted Handoff | Temporary node failure | Write to available node; replay when target recovers |
| Read Repair | Stale replica detection | Opportunistic consistency fix on every read |
| Bloom Filters | SSTable read optimization | Avoid disk reads for keys not in SSTable |
| Write-Ahead Log | Durability before MemTable | Crash recovery: replay WAL to restore MemTable |
#8. Failure Handling & Edge Cases
#What Happens When X Fails?
| Failure | Impact | Mitigation |
|---|---|---|
| Single node down | 1/N keys unavailable for strong consistency | Hinted handoff; sloppy quorum; read from remaining replicas |
| Network partition | Split brain between node groups | AP system: both sides accept writes; resolve conflicts on heal |
| Disk failure | Node's data lost | Rebuild from replicas; copy SSTables from 2 healthy replicas |
| Slow node (straggler) | Tail latency spike | Read from fastest R replicas; ignore slow one |
| MemTable crash | Recent writes lost | Replay from WAL (write-ahead log); no data loss |
| Compaction stall | Read amplification increases | Alert on SSTable count; auto-trigger emergency compaction |
#Edge Cases
- Hot key (millions of reads) β Client-side caching; read from local replica only (relaxed consistency)
- Tombstone accumulation β Garbage collect tombstones after grace period (default 10 days)
- Clock skew for LWW β Use hybrid logical clocks (HLC); combine physical + logical timestamps
- Data corruption β Checksums per SSTable block; detect on read; repair from replica
- Cascading failure β Rate limit rebalancing; don't move too many vnodes at once
#9. Monitoring & Observability
#Key Metrics
| Metric | Target | Alert Threshold |
|---|---|---|
| Read P99 latency | < 5ms (eventual) | > 20ms |
| Write P99 latency | < 10ms | > 50ms |
| MemTable flush frequency | Every 2β5 min | > 10/min (too much write pressure) |
| SSTable count per node | < 20 per level | > 50 (compaction behind) |
| Gossip convergence time | < 10s | > 60s |
| Replication lag | < 1s | > 10s |
| Node disk usage | < 80% | > 90% |
| Bloom filter false positive rate | < 1% | > 5% |
#Alerting Strategy
- P0 (Page immediately): Quorum failures (can't get W or R acks), node unresponsive > 5 min, data corruption detected
- P1 (15 min response): Compaction backlog growing, replication lag > 30s, disk usage > 85%
- P2 (Next business day): Bloom filter FP rate rising, gossip convergence slow, uneven key distribution
#SLAs / SLOs
Read API (eventual): 99.99% availability, P99 < 5ms
Read API (strong): 99.95% availability, P99 < 10ms
Write API: 99.99% availability, P99 < 10ms
Durability: 99.999999999% (11 nines with 3x replication)
Data consistency: Convergence within 10s for eventual; immediate for quorum#10. Trade-off Summary
| Decision | Chose | Over | Because |
|---|---|---|---|
| Partitioning | Consistent hashing with vnodes | Range-based | Even distribution; minimal disruption on node add/remove |
| Storage engine | LSM-tree | B-tree | Optimized for write-heavy; sequential I/O for writes |
| Default consistency | AP (available + partition tolerant) | CP | Most KV workloads prioritize availability over strict consistency |
| Conflict resolution | Last-Write-Wins (LWW) | Vector clocks with app merge | Simpler; acceptable for most use cases; vector clocks optional |
| Failure detection | Gossip protocol | Centralized heartbeat | No single point of failure; scales to 1000+ nodes |
| Anti-entropy | Merkle trees | Full data comparison | O(log N) comparison; transfer only divergent keys |
| Replication | Sloppy quorum + hinted handoff | Strict quorum | Higher availability; accept brief inconsistency during failures |
| Compaction | Leveled compaction | Size-tiered | Better read performance; controlled space amplification |
#11. Extensions & Follow-ups
#What Would You Add With More Time?
- Secondary Indexes β Allow queries beyond primary key (like DynamoDB GSI)
- Multi-Region Replication β Cross-datacenter async replication with conflict resolution
- Transactions β Lightweight transactions (compare-and-swap) for conditional writes
- Auto-Scaling β Automatic node addition/removal based on load
- Tiered Storage β Hot data on SSD, warm on HDD, cold on S3
- Change Data Capture (CDC) β Stream mutations to downstream consumers
#How Would This Change at 100x Scale?
- 100M ops/sec: 1000+ nodes; client-side caching mandatory; request routing at NIC level (DPDK/kernel bypass)
- 1PB dataset: Tiered storage with automatic migration; aggressive compaction scheduling
- Multi-region: Per-region clusters with async cross-region replication; conflict resolution via CRDTs
- Hot keys: Dedicated cache layer (like DAX for DynamoDB); key-level replication factor override
#12. Cross-References
#Related Topics in This Repo
| Topic | Connection |
|---|---|
| URL Shortener (#1) | Uses Redis (a KV store) for URL cache |
| Rate Limiter (#2) | Redis counters are KV store operations |
| Unique ID Generator (#4) | KV store needs unique keys for internal coordination |
| Distributed Cache (#15) | Cache is a specialized volatile KV store |
#Building Blocks Used
- PostgreSQL β Comparison point for SQL vs KV trade-offs
- Redis β In-memory KV store used as reference implementation for caching layer
- Cassandra β Wide-column store with similar distributed architecture (consistent hashing, gossip, tunable consistency)
#Concepts Used
- Consistent Hashing β Key partitioning across nodes with virtual nodes
- Consistency Models β Tunable consistency via quorum (W+R>N)
- Bloom Filters β SSTable read optimization; avoid unnecessary disk reads
- Sharding β Consistent hash-based partitioning of keyspace