Caching Patterns
Pick caching strategy by consistency tolerance, read/write shape, and failure behavior.
📌 Key IdeaMost systems start with cache-aside. Move to write-through when freshness matters, and only use write-behind for specialized high-write workloads with strong durability controls.
Why It Matters
- Caching decisions can improve performance dramatically or create stale-data outages.
- Pattern choice determines read latency, write cost, and failure behavior.
- Interview rounds often test whether you can explain these trade-offs clearly.
Visual
🗄 Cache strategy map
Cache-aside (lazy): read miss -> DB -> put cache ✅ simple, common ⚠ stale windows + stampede risk Write-through: write -> DB + cache ✅ fresher reads ⚠ write latency / dual-write handling Write-behind: write -> cache, async flush DB ✅ high write throughput ⚠ durability + replay complexity
Pattern choice is mostly consistency vs latency trade-off, not coding style.
When to Use
- Cache-aside for read-heavy APIs and hot-key lookups.
- Write-through when stale reads are costly after writes (profiles, product metadata).
- Write-behind for buffered telemetry/counters where eventual persistence is acceptable.
- Add stampede protection for high-fanout keys.
When NOT to Use
- Strictly consistent financial invariants without reconciliation controls.
- Low-traffic endpoints where cache complexity gives little benefit.
- Write-behind without durable queue/log and replay strategy.
Code Variations
1️⃣ Cache-aside (default)java
UserProfile get(String userId) {
UserProfile p = redis.get(userId);
if (p != null) return p;
p = repo.fetch(userId);
redis.set(userId, p, ttlWithJitter());
return p;
}Simple and common; combine with single-flight for hot misses.
2️⃣ Write-throughjava
@Transactional
void updateProfile(UserProfile p) {
repo.save(p);
redis.set("user:" + p.id(), p, Duration.ofMinutes(10));
}Keeps cache warm/fresh after write path; write latency is slightly higher.
3️⃣ Stampede protection (single-flight)java
return singleFlight.computeIfAbsent(userId, k -> {
UserProfile fresh = repo.fetch(k);
redis.set(k, fresh, ttlWithJitter());
return fresh;
});Coalesces concurrent misses so DB sees one fetch per key burst.
4️⃣ Write-behind sketchjava
void incrementView(String key) {
redis.incr(key);
outbox.append(new FlushCounterEvent(key)); // durable event
}Needs durable event log + idempotent flusher to avoid loss/duplication.
Caching strategy comparison
| Aspect | Cache-aside | Write-through | Write-behind |
|---|---|---|---|
| Read latency | ✅ Low on hit; miss pays DB | ✅ Low and typically warm | ✅ Low if cache available |
| Write latency | ✅ Lowest | ⚠ Medium (DB + cache) | ✅ Lowest in request path |
| Freshness after write | ⚠ Depends on invalidation | ✅ Usually freshest | ⚠ Eventual |
| Failure complexity | ⚠ Invalidation/stampede | ⚠ Dual-write ordering | ❌ Highest (durability/replay) |
| Best fit | General read-heavy APIs | Read-after-write sensitive paths | High-write buffered workloads |
Common Confusions Cleared Up
Cache-aside vs Write-through
Cache-aside optimizes simplicity and read path. Write-through optimizes read freshness right after writes at the cost of more complex write flow.
TTL invalidation vs event invalidation
TTL is simple but introduces stale windows. Event invalidation is fresher but requires reliable delivery and ordering.
Interview Q&A
Quick Cheat Sheet
DefaultCache-aside + TTL jitter + explicit invalidation.
Key designNamespaced + versioned keys (
user:v2:{id}).ObserveHit ratio, miss burst, stale-read incidents, evictions.
Protect DBSingle-flight + backpressure + graceful stale fallback.