Requirements & scope
Problem Statement & Requirements
Design a distributed, in-memory caching system that sits between application servers and persistent storage to dramatically reduce read latency, offload databases, and absorb traffic spikes — similar to Redis, Memcached, or Amazon ElastiCache.
Functional Requirements
- GET / SET / DELETE — basic key-value operations with sub-millisecond latency
- TTL (Time-to-Live) — automatic key expiration
- Rich data structures — strings, hashes, lists, sets, sorted sets, bitmaps, HyperLogLog, streams
- Atomic operations — INCR, DECR, SETNX (set-if-not-exists), CAS (compare-and-swap)
- Pub/Sub — publish-subscribe messaging for cache invalidation and real-time events
- Lua scripting — execute atomic multi-step operations server-side
- Persistence — optional RDB snapshots and AOF (append-only file) for durability
- Cluster mode — automatic sharding across nodes with rebalancing
- Replication — master-replica for high availability and read scaling
Non-Functional Requirements
- Ultra-low latency — < 1 ms for cache hits (p99 < 2 ms)
- High throughput — 100K-1M+ operations/second per node
- High availability — 99.999% uptime with automatic failover
- Horizontal scalability — add nodes to increase capacity linearly
- Memory efficiency — maximize useful data per GB of RAM
- Consistency — strong consistency for single-key operations; eventual consistency across replicas
- Predictable performance — no garbage collection pauses, no swap, bounded tail latency
Out of Scope
- Full database replacement (we're designing a cache, not a primary data store)
- SQL query caching (application-level concern)
- Multi-region active-active replication (mention as extension)
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Read operations / second | 10M RPS (across cluster) |
| Write operations / second | 1M RPS (10:1 read-write ratio) |
| Peak reads / second | 30M RPS (3x average) |
| Number of application servers | 5,000 |
| Connections per app server | ~20 (connection pool) |
| Total client connections | ~100,000 |
Storage
| Metric | Value |
|---|---|
| Total unique keys | 5 billion |
| Average key size | 50 bytes |
| Average value size | 500 bytes |
| Average entry (key + value + metadata overhead) | ~650 bytes → round to 1 KB |
| Total working set | 5B × 1 KB = ~5 TB |
| Hot data (20% accessed in any hour) | ~1 TB |
| Memory per node (typical) | 64-128 GB usable |
| Number of nodes for 5 TB | ~50-80 primary nodes |
| With replication (1 replica each) | 100-160 total nodes |
Bandwidth
| Metric | Value |
|---|---|
| Read bandwidth (10M × 500 B avg value) | ~5 GB/s cluster-wide |
| Write bandwidth (1M × 500 B) | ~500 MB/s cluster-wide |
| Per-node bandwidth (80 nodes) | ~70 MB/s per node |
| Replication traffic | ~500 MB/s (async to replicas) |
Hardware Estimate (Per Node)
| Component | Spec |
|---|---|
| CPU | 8-16 cores (Redis is mostly single-threaded per shard, but I/O threads help) |
| RAM | 128 GB (64 GB usable for data, rest for OS/fragmentation/fork overhead) |
| Disk | 500 GB NVMe SSD (for RDB snapshots + AOF persistence) |
| Network | 25 Gbps NIC |
| Nodes in cluster | 80 primary + 80 replica = 160 total |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Component Breakdown
Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
API Design
Core Operations
# String operations
SET key value [EX seconds] [PX milliseconds] [NX|XX]
GET key → value or nil
MGET key1 key2 key3 → [val1, val2, val3] (batch read)
MSET key1 val1 key2 val2 → OK (batch write, atomic)
INCR key → new_value (atomic increment)
SETNX key value → 1 if set, 0 if already exists (mutex primitive)
# Hash operations (object caching)
HSET user:123 name "Alice" age 30 city "Tokyo"
HGET user:123 name → "Alice"
HGETALL user:123 → {name: "Alice", age: "30", city: "Tokyo"}
# List operations (queue, feed)
LPUSH queue:emails msg1 msg2 → push to head
RPOP queue:emails → pop from tail (FIFO queue)
LRANGE feed:user:123 0 19 → get first 20 items
# Set operations (unique collections)
SADD tags:article:456 "redis" "cache" "design"
SISMEMBER tags:article:456 "redis" → 1 (true)
SINTER tags:article:456 tags:article:789 → intersection
# Sorted set operations (leaderboards, priority queues)
ZADD leaderboard 9500 "player-A" 8200 "player-B" 9800 "player-C"
ZREVRANGE leaderboard 0 9 WITHSCORES → top 10 players
ZRANK leaderboard "player-A" → rank
# TTL and expiration
EXPIRE key 3600 → expires in 1 hour
TTL key → seconds remaining (-1 = no expiry, -2 = doesn't exist)
# Pub/Sub
PUBLISH channel:inventory msg → broadcast to all subscribers
SUBSCRIBE channel:inventory → receive messages
# Atomic transactions (MULTI/EXEC)
MULTI
SET account:A:balance 900
SET account:B:balance 1100
EXEC → atomic execution of both commands
# Lua scripting (complex atomic operations)
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else return 0 end"
1 lock:resource "lock-token-123"
→ atomic compare-and-delete (used for distributed locks)Application-Level Caching Patterns (Client-Side)
# Cache-Aside (Lazy Loading) — most common
def get_user(user_id):
cached = cache.get(f"user:{user_id}")
if cached:
return deserialize(cached)
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
cache.set(f"user:{user_id}", serialize(user), ex=3600)
return user
# Write-Through
def update_user(user_id, data):
db.update("UPDATE users SET ... WHERE id = ?", data, user_id)
cache.set(f"user:{user_id}", serialize(data), ex=3600)
# Write-Behind (Write-Back)
def update_user_async(user_id, data):
cache.set(f"user:{user_id}", serialize(data), ex=3600)
queue.enqueue("db_write", {"user_id": user_id, "data": data})
# Background worker flushes to DB in batches
# Read-Through (cache library handles miss transparently)
# Configured at infrastructure level, not application codeData model
Data Model
Internal Memory Layout
Internal Memory LayoutExcalidraw diagram · editable shapes · reveal step by stepExplore
Data Structure Encodings (Memory Optimization)
Data Structure Encodings (Memory Optimization)Excalidraw diagram · editable shapes · reveal step by stepExplore
Cluster Slot Assignment
Cluster Slot AssignmentExcalidraw diagram · editable shapes · reveal step by stepExplore
Core design decisions
Core Design Decisions
Decision 1: Single-Threaded vs Multi-Threaded Execution
This is the most debated design choice in cache systems.
Option A: Single-Threaded Command Execution (Redis)
Option A: Single-Threaded Command Execution (Redis)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| No locks, no race conditions | Can't use multiple cores for one shard |
| Atomic operations are free | Single slow command (KEYS *) blocks everything |
| Simple, predictable latency | Max ~300K ops/sec per shard for complex ops |
| Easier to reason about | Fork for persistence competes for memory |
Option B: Multi-Threaded (Memcached / Dragonfly)
Option B: Multi-Threaded (Memcached / Dragonfly)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Uses all CPU cores per node | Lock contention on hot keys |
| Higher single-node throughput | Multi-key atomicity requires distributed locks |
| Better for large values (parallel serialization) | Harder to reason about race conditions |
| Fewer nodes for same total throughput | More complex implementation |
Recommendation
Single-threaded command execution (Redis model) for most use cases:
- Simplicity and correctness trump raw throughput per node
- Network is the bottleneck, not CPU — single thread saturates the NIC
- Scale horizontally via sharding (more shards, more nodes)
- I/O threads in Redis 6+ handle the socket bottleneck
Multi-threaded (Dragonfly) when:
- You want to minimize node count (cost optimization)
- Workload is dominated by large values (serialization-heavy)
- You can tolerate more complex operational model
Decision 2: Eviction Policies
Decision 2: Eviction PoliciesExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 3: Persistence Strategy
Decision 3: Persistence StrategyExcalidraw diagram · editable shapes · reveal step by stepExplore
Consistent Hashing & Data Distribution
Why Consistent Hashing
Why Consistent HashingExcalidraw diagram · editable shapes · reveal step by stepExplore
Redis Cluster Approach (Hash Slots)
Redis Cluster Approach (Hash Slots)Excalidraw diagram · editable shapes · reveal step by stepExplore
Request flows
Detailed Flow Diagrams
Cache-Aside Read Flow
Cache-Aside Read FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Write-Invalidate Flow (Cache + DB Consistency)
Write-Invalidate Flow (Cache + DB Consistency)Excalidraw diagram · editable shapes · reveal step by stepExplore
Cluster Redirect Flow (MOVED / ASK)
Cluster Redirect Flow (MOVED / ASK)Excalidraw diagram · editable shapes · reveal step by stepExplore
Failover Flow (Sentinel / Cluster)
Failover Flow (Sentinel / Cluster)Excalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Caching Patterns Deep Dive
Pattern Comparison
Pattern ComparisonExcalidraw diagram · editable shapes · reveal step by stepExplore
When to Use Which Pattern
| Pattern | Best For | Avoid When |
|---|---|---|
| Cache-Aside | General-purpose caching; read-heavy workloads | Write-heavy with strong consistency needs |
| Read-Through | Same as cache-aside but with cleaner code | Custom loading logic varies per key type |
| Write-Through | Read-after-write consistency required | Write-heavy but data rarely re-read |
| Write-Behind | Write-heavy, can tolerate brief data loss risk | Financial / transactional data |
| Refresh-Ahead | Predictable hot keys (config, leaderboards) | Long-tail access patterns |
Redis vs Memcached — When to Use Which
Redis vs Memcached — When to Use WhichExcalidraw diagram · editable shapes · reveal step by stepExplore
Advanced design
Workshop note · added for the website’s common reading format
Stress-test the boundary between Cache primaries and the data layer. Consider partitioning, ownership changes, and the effect of stale metadata. Explain the invariant that must survive a retry and the data that can be recomputed.
Use the source's subsystem deep dives as the detailed extension of this baseline; the progressive diagram is deliberately a teaching abstraction rather than a complete deployment specification.
Edge cases
Handling Edge Cases
Cache Stampede / Thundering Herd
Problem: Hot key expires → 10,000 threads all miss simultaneously
→ all 10,000 query DB → DB overwhelmed
Solution 1: Distributed Lock (Setnx)
Thread 1: SETNX lock:user:123 "1" EX 5 → OK (acquired)
Thread 2: SETNX lock:user:123 "1" EX 5 → FAIL (wait)
Thread 3: SETNX lock:user:123 "1" EX 5 → FAIL (wait)
...
Thread 1: fetches from DB → SET user:123 → DEL lock:user:123
Threads 2,3,...: retry GET → HIT
Solution 2: Probabilistic Early Expiration (best for high-throughput)
Actual TTL = 3600s
Each read: if current_time > (expire_time - delta * beta * ln(rand()))
→ preemptively refresh (only one thread statistically wins)
→ Cache never fully expires under load
Solution 3: Background Refresh
TTL = 3600s, but at 3000s (80% of TTL), background thread refreshes
→ Key never expires → zero stampede
→ Requires tracking hot keysHot Key Problem
Problem: One key gets 100K reads/sec (celebrity profile, flash sale item)
→ Single shard/node overwhelmed
Solution 1: Local In-Process Cache (L1)
App server caches hottest keys in process memory (HashMap with TTL)
→ Short TTL (5-10s) to limit staleness
→ Zero network overhead for hits
→ Each app server has its own copy
Solution 2: Key Replication (read replicas for hot keys)
Detect hot key → replicate to N random shards with suffix:
"user:celebrity" → "user:celebrity#r1", "user:celebrity#r2", ...
Client randomly picks one → distributes load across N nodes
Solution 3: Shard-level Read Replicas
Hot shard gets more read replicas (3 → 6)
Client reads from any replica → load distributed
Detection: track per-key access frequency at proxy layer
→ Key exceeding 10K RPS threshold → trigger hot key mitigationCache Penetration
Cache PenetrationExcalidraw diagram · editable shapes · reveal step by stepExplore
Cache Avalanche
Problem: Many keys expire at the SAME time (e.g., all set at startup with same TTL)
→ Massive simultaneous cache miss → DB overload
Solution: Jittered TTL
base_ttl = 3600
jitter = random(0, 600) // ±10 minutes
actual_ttl = base_ttl + jitter
→ Expirations spread over 10-minute window instead of all at once
Solution: Staggered warming
On service restart, don't load all cache at once
→ Load in batches of 1000, with 100ms delay between batches
→ Prevents overwhelming both cache and DBCache-Database Consistency
Cache-Database ConsistencyExcalidraw diagram · editable shapes · reveal step by stepExplore
Tradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Threading | Single-threaded (Redis) | Multi-threaded (Dragonfly) | Single-threaded | Simplicity, atomicity for free, network-bound anyway; scale via sharding |
| Eviction | LRU | LFU | allkeys-lfu (Redis 4.0+) | Resists scan pollution; better hit ratio for skewed distributions |
| Persistence | RDB snapshots | AOF log | Hybrid RDB+AOF | Fast recovery (RDB) + minimal loss (AOF everysec) |
| Cluster Topology | Consistent hashing (ring) | Fixed hash slots (Redis Cluster) | Hash slots | Simpler, deterministic, fine-grained slot migration |
| Caching Pattern | Cache-aside | Write-through | Cache-aside | Only caches accessed data; cache failure = degraded, not broken |
| Invalidation | Update cache on write | Delete cache on write | Delete | Avoids race conditions; idempotent; simpler |
| Replication | Synchronous | Asynchronous | Async | Sub-ms latency; sync replication adds 1+ RTT per write |
| Serialization | JSON | MessagePack / Protobuf | Protobuf | 3-5x smaller than JSON; faster serialization; worth the complexity |
| Connection | Per-request | Connection pool | Pool (20-50 per app) | Eliminates TCP/TLS handshake per request; reduces server socket count |
| Hot Key | Single shard handles all | L1 local cache + key replication | L1 cache + detection | Zero network for hottest keys; replicas for extreme cases |
Reliability & fault tolerance
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Graceful Degradation
Level 1 (single replica failure):
→ Primary continues serving. No impact.
→ New replica auto-provisioned.
Level 2 (single primary failure):
→ Sentinel promotes replica in 5-15s.
→ Brief connection errors during failover.
→ Clients reconnect to new primary via Sentinel.
Level 3 (multiple shard failures):
→ Affected key ranges unavailable.
→ Application circuit breaker activates → fall back to DB.
→ Other shards continue normally.
Level 4 (full cache cluster down):
→ ALL reads go to database.
→ Database must handle full load (capacity plan for this!).
→ Rate limiting to protect DB.
→ Cache rebuilt on restart (warm-up takes minutes to hours).
Key design principle:
Cache is an OPTIMIZATION, not a requirement.
The system MUST function (slower) without cache.
Never put data ONLY in cache unless you accept loss.Data Loss Scenarios
Data Loss ScenariosExcalidraw diagram · editable shapes · reveal step by stepExplore
Production architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Distributed Cache from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Cache primaries with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
Interview Tips
Start with caching patterns — Explain cache-aside vs write-through vs write-behind. Most interviews expect you to know these cold. Draw the flow for cache-aside (check cache → miss → query DB → populate cache).
Discuss consistency immediately — "How do you keep cache and DB in sync?" Explain why DELETE is safer than UPDATE, the double-write race condition, and CDC as the gold standard.
Cache stampede is the #1 follow-up question — Have the three solutions ready: distributed lock (SETNX), probabilistic early expiration, and background refresh. Know when each applies.
Know your eviction policies — Don't just say "LRU". Explain why approximate LRU (sampling) is used over true LRU (no linked list overhead), and when LFU beats LRU (scan resistance).
Hot key problem — Interviewers love this. Solution: L1 in-process cache (5-10s TTL) for zero-network hot reads, then key replication across shards for extreme cases.
Explain why single-threaded works — "Isn't single-threaded slow?" No — in-memory ops are 100-500 ns, network is 50K+ ns. CPU isn't the bottleneck. Single-thread gives atomicity for free. Scale horizontally via sharding.
Cluster topology — Explain hash slots (16,384), how slot migration works live with MOVED/ASK redirects, and why cross-AZ replica placement matters.
Persistence tradeoffs — RDB (fast recovery, data loss) vs AOF (minimal loss, slower recovery) vs hybrid (best of both). Mention fork overhead and why replicas should handle RDB saves.
Size your cache — Don't guess. Calculate: key count × (key size + value size + overhead). Remember jemalloc fragmentation (~10-20%). Account for replication doubling the node count.
End with operational concerns — Memory monitoring (never let it hit maxmemory without an eviction policy), slow log analysis, replication lag alerting, and the importance of connection pooling (100K app connections → 1K with pools → manageable for cache nodes).