Distributed Caching Strategies: Redis, Memcached, and Application-Level Patterns
Caching is the first tool you reach for when scaling. It is also the first thing that breaks catastrophically when done wrong. These patterns prevent the common failures.
Cache-Aside (Lazy Loading)
The application checks cache first, falls back to database, then populates cache. Simple and effective, but leaves the cache cold after eviction and risks thundering herd on popular keys.
Thundering Herd Protection
async def get_with_lock(key: str):
value = await cache.get(key)
if value: return value
# Only one request rebuilds the cache
lock = await cache.set(f"lock:{key}", "1", nx=True, ex=5)
if lock:
value = await database.query(key)
await cache.set(key, value, ex=3600)
return value
# Others wait briefly and retry
await asyncio.sleep(0.1)
return await cache.get(key)
Cache Invalidation: The Hard Problem
Event-driven invalidation beats TTL-based expiry. When data changes, publish an event. Cache subscribers evict the relevant keys immediately. You get consistency measured in milliseconds instead of minutes.
Multi-Layer Caching
L1 (in-process, Caffeine) → L2 (Redis) → L3 (CDN). Hot keys never leave the application process. Warm keys hit Redis. Cold keys go to the database. This three-tier approach handles millions of requests per second.