Comprehensive Cache Architecture: Patterns, Invalidation, and High-Scale Pitfalls
Deep-dive into Cache-Aside, Write-Through, Write-Behind, Cache Stampede / Thundering Herd mitigation, and distributed Redis cluster strategies.
By Amr Samir• August 21, 2026• 1 min
Comprehensive Cache Architecture: Patterns, Invalidation, and High-Scale Pitfalls
1. Why Caching Is More Than Just a Key-Value Store
Caching is the most powerful technique for reducing database load and network latency. However, introducing a cache transforms a single source of truth into a distributed system with potential consistency anomalies, cache stampedes, and stale reads.
code
+-------------+ 1. Read Cache +-------------+
| Application | ----------------------> | Redis |
+-------------+ +-------------+
| |
| 2. Cache Miss | Return Data
v v
+-------------+
| Database |
+-------------+
2. Core Caching Patterns
1. Cache-Aside (Lazy Loading)
The application first queries Redis. If missed, it queries the database and populates Redis with an expiration TTL.
- Pros: Resilient to Redis node failure; caches only active hot data.
- Cons: Cache miss penalty on first access; potential data drift if updates don't invalidate.
2. Write-Through & Write-Behind (Write-Back)
- Write-Through: Updates are written synchronously to cache and database together.
- Write-Behind: Updates are written to cache immediately, then queued to be flushed asynchronously to the database.
3. The 3 Classic High-Scale Pitfalls
code
1. Cache Stampede (Thundering Herd)
Key expires -> 10,000 concurrent requests all miss -> 10,000 queries hit DB simultaneously -> DB CRASH!
Fix: Mutex Lock / Singleflight pattern or probabilistic early expiration (XFetch).
2. Cache Penetration
Requests for non-existent IDs bypass cache completely and hit DB continuously.
Fix: Store null values with short TTL, or use Bloom Filters.
3. Cache Avalanche
Thousands of keys set with the exact same 3600s TTL expire at the exact same second.
Fix: Add random jitter to TTLs: TTL = 3600 + Math.floor(Math.random() * 300).
4. Mutex Locking Implementation (Preventing Stampedes)
typescript
async function getWithMutex(key: string, fetchDb: () => Promise<any>, ttlSeconds: number) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, "1", "NX", "EX", 10);
if (acquired) {
try {
const data = await fetchDb();
await redis.set(key, JSON.stringify(data), "EX", ttlSeconds);
return data;
} finally {
await redis.del(lockKey);
}
} else {
// Wait briefly and retry from cache
await new Promise((resolve) => setTimeout(resolve, 50));
return getWithMutex(key, fetchDb, ttlSeconds);
}
}
5. Summary
- Always configure memory eviction policies (e.g.,
volatile-lruorallkeys-lru). - Apply jitter to TTLs to avoid synchronized expiration storms.
- Protect your primary database from cold-start stampedes with distributed locking or singleflight loaders.
Recommended Posts
Related Projects
YallaBaytak - Multi-Role Real Estate Operating Platform
Real estate operating system featuring multi-role workflows, WhatsApp alerts, and geospatial mapping in Cairo & Giza.