Mental model
The Cache-Aside (Lazy Loading) pattern positions an in-memory data store (Redis) beside the primary database. The application queries Redis first; on a cache miss, it reads from PostgreSQL, writes the result to Redis with a TTL, and returns the response.
Theory
When a popular cache key expires under high concurrency (e.g. 1,000 concurrent requests/sec), all workers encounter a cache miss simultaneously and hammer PostgreSQL. This is the Thundering Herd (Cache Stampede) problem.
Defense requires acquiring a distributed Redis lock (redis.set(lock_key, "1", nx=True, ex=5)) so that only ONE worker queries PostgreSQL while other workers wait briefly or receive stale cached data.
Alternatives and trade-offs
- Read-Through Cache: Cache layer intercepts database reads transparently; complex middleware setup.
- Cache-Aside: Simple application-level caching control; risks serving stale data if invalidation logic fails on state updates.
Failure modes and misconceptions
- Missing Cache Invalidation on Writes: Updating a user record in PostgreSQL without executing
redis_client.delete(cache_key)causes users to see stale data until TTL expires. - Infinite Cache Lock Deadlock: Acquiring a distributed lock without setting an explicit
ex(expiration TTL) locks out all concurrent workers permanently if the process crashes mid-query.
Decision scenario
Implement Cache-Aside with Redis set(nx=True, ex=5) distributed locking on high-traffic product catalog endpoints to prevent database crash loops caused by cache stampedes.
Learning outcomes
- Implement the Cache-Aside pattern with explicit Redis TTL expiration.
- Defend against Thundering Herd / Cache Stampede spikes using Redis distributed locks.
- Invalidate cached data deterministically during database mutation operations.
Trade-offs
Redis caching dramatically reduces database query load and response latency, but increases system state complexity and requires strict cache invalidation rules.