01

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)
02

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
03

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
04

API & contracts

API Design

Core Operations

EXAMPLE
# 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)

python
# 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 code
05

Data 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
06

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
07

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
08

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
09

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.

10

Edge cases

Handling Edge Cases

Cache Stampede / Thundering Herd

EXAMPLE
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 keys

Hot Key Problem

EXAMPLE
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 mitigation

Cache Penetration

Cache PenetrationExcalidraw diagram · editable shapes · reveal step by stepExplore

Cache Avalanche

EXAMPLE
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 DB

Cache-Database Consistency

Cache-Database ConsistencyExcalidraw diagram · editable shapes · reveal step by stepExplore
11

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
12

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

EXAMPLE
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
13

Production architecture

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
14

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.

15

Interview playbook

Interview Tips

  1. 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).

  2. 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.

  3. 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.

  4. 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).

  5. 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.

  6. 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.

  7. Cluster topology — Explain hash slots (16,384), how slot migration works live with MOVED/ASK redirects, and why cross-AZ replica placement matters.

  8. 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.

  9. 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.

  10. 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).