Caching Strategies#
Problem statement (interviewer prompt)
Walk through the four canonical cache patterns (cache-aside, read-through, write-through, write-back) for a read-heavy product. Explain when to use each, how to size the cache, and how to handle stampedes, hot keys, and invalidation across multiple servers.
flowchart LR
C([Client])
A[App]
K[(Cache)]
D[(DB)]
C --> A
A -->|1 read| K
K -. miss .-> A
A -->|2 load| D
D --> A
A -->|3 fill| K
A --> C
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class C client;
class A service;
class K,D datastore;
The four main patterns are cache-aside, read-through, write-through, and write-back.
flowchart TB
subgraph Tiers[Cache Tiers]
B[Browser Cache]
CDN[CDN / Edge Cache]
REV[Reverse Proxy<br/>Varnish/NGINX]
APP[App Local<br/>Caffeine/Guava]
DC[Distributed Cache<br/>Redis/Memcached]
DBC[DB Buffer Pool]
end
subgraph Patterns[Patterns]
CA[Cache-Aside<br/>look-aside]
RT[Read-Through]
WT[Write-Through]
WB[Write-Back<br/>write-behind]
WA[Write-Around]
REF[Refresh-Ahead]
end
subgraph Eviction[Eviction]
LRU
LFU
TLRU[TTL+LRU]
ARC
TwoQ[2Q / TinyLFU]
end
subgraph Failures[Failure Modes]
STAMP[Cache Stampede<br/>thundering herd]
PEN[Cache Penetration<br/>key never exists]
AVAL[Cache Avalanche<br/>mass expiry]
INV[Stale data /<br/>invalidation bugs]
end
subgraph Mitigations[Mitigations]
LOCK[Single-flight /<br/>distributed lock]
BLOOM[Bloom filter for<br/>missing keys]
JIT[Jittered TTL]
NEG[Negative caching]
VER[Versioned keys]
end
Client --> B --> CDN --> REV --> APP --> DC --> DBC
Client --> Patterns
Patterns --> Eviction
Failures --- Mitigations
STAMP -.->|fix| LOCK
PEN -.->|fix| BLOOM
PEN -.->|fix| NEG
AVAL -.->|fix| JIT
INV -.->|fix| VER
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class CDN,REV edge;
class CA,RT,WT,WB,WA,REF,TLRU,STAMP,PEN,AVAL,INV,LOCK,BLOOM,JIT,NEG,VER service;
class B,APP,DC,DBC,TwoQ cache;
Pattern semantics#
| Pattern | Read | Write | Notes |
|---|---|---|---|
| Cache-aside | app reads cache, falls back to DB, fills cache | app writes DB, invalidates cache | most common |
| Read-through | cache transparently loads from DB on miss | - | cache library does the load |
| Write-through | - | app writes cache → cache writes DB synchronously | consistent, slower writes |
| Write-back | - | app writes cache → cache async-flushes to DB | fast writes, durability risk |
| Write-around | - | app writes DB, cache filled lazily on read | avoids cache pollution by one-time writes |
| Refresh-ahead | proactively refresh hot keys before TTL | - | predictive |
Invalidation#
- TTL (simplest, drifts).
- Explicit on write (
DEL, pub/sub). - Versioned key (
user:42:v17) - never invalidate, just bump version. - Change-Data-Capture (Debezium) → cache invalidator.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Load balancer / GSLB | L4/L7 traffic distribution and failover | load-balancer |
HLD |
CDN | edge caching for static assets | cdn |
HLD |
Cache strategies | cache-aside, read/write-through, eviction | caching-strategies |
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
Probabilistic data structures | Bloom, HLL, Count-Min, MinHash, t-digest | probabilistic-data-structures |
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
HLD |
Change Data Capture | WAL/binlog tailing, outbox publishing | change-data-capture |
LLD |
Structural patterns | Adapter, Decorator, Facade, Proxy, Composite | structural-patterns |
Quick reference#
Hierarchy (latency)#
- L1 CPU cache: 0.5-10 ns
- RAM: 100 ns
- App-local (Caffeine): ~µs
- Remote cache (Redis same DC): 0.3-1 ms
- SSD: 100 µs
- HDD: 10 ms
- Cross-DC: 50-200 ms
When NOT to cache#
- Mutates frequently and read-immediately-after-write semantics required.
- Per-user data with tiny re-read probability.
- Already cheap (< 1 ms) reads.
Sizing#
- Working-set estimation: 80/20 rule - cache hot 20% to hit 80%.
- Hit ratio target: 90-99% for read-heavy.
Concurrency pitfalls#
- Stampede: 1000 RPS hit cache miss simultaneously → all query DB.
Fix: single-flight (one fetches, others wait),
EX NXlock with backoff, or pre-compute. - Inconsistency window: DB updated, cache not yet evicted. Mitigate with short TTL or CDC.
Choosing TTL#
- Tolerance for staleness × refresh cost.
- Add jitter (
TTL ± 10%) to avoid mass expiry.
Refs#
- Facebook memcached at scale (NSDI '13), Netflix EVCache, DynamoDB DAX, TinyLFU / W-TinyLFU paper (Caffeine).
FAQ#
What are the four main caching strategies?#
Cache-aside (lazy load), read-through (cache loads on miss), write-through (write to cache and DB synchronously), and write-back (write to cache first, flush to DB async).
What is the difference between cache-aside and read-through?#
In cache-aside the application checks the cache and loads from the DB on miss. In read-through the cache itself loads from the DB on miss, so the app only talks to the cache.
How do you prevent cache stampede?#
Use request coalescing with a single-flight lock, add jitter to TTLs, refresh ahead of expiry, or pre-warm hot keys. Probabilistic early expiration also smooths the renewal pattern.
Write through vs write back, which is safer?#
Write-through is safer because data is durable in the DB before ack. Write-back is faster but risks data loss if the cache crashes before flush. Use write-back only when the DB can rebuild from a log.
When should I use cache-aside?#
Cache-aside is the default for most read-heavy services. It is simple, the cache stays optional, and the app controls what to cache. Use it unless you specifically need write-through consistency.
Related Topics#
- Consistent Hashing: distributed caches use consistent hashing to assign keys to cache nodes
- CDN: CDNs are a global caching layer built on the same invalidation and eviction principles
- Database Sharding: caching strategies reduce read pressure on sharded databases
Further reading#
Curated, high-credibility sources for going deeper on this topic.
- 📄 Paper - Facebook - Scaling Memcache at Facebook (NSDI '13)
- ✍️ Blog - Netflix - EVCache: a distributed in-memory data store
- 📄 Paper - TinyLFU: a highly efficient cache admission policy (Einziger et al.)
- 📑 Docs - AWS - Caching strategies and best practices