Command Palette

Search for a command to run...

Level 2 · 25 min

Cache Patterns

Cache patterns define how the application interacts with the cache and the underlying database. The right pattern depends on read/write ratios, consistency requirements, and tolerable staleness.

Cache-Aside

Cache-Aside gives the application explicit control and maximum observability — cache hits and misses are visible in application code and metrics. The cold-start problem is real: after a Redis failover or explicit FLUSHALL, 100% of requests fall through to the database simultaneously. Mitigation: a cache-warming job that pre-populates hot keys on startup using the same key schema as the read path. Key schema discipline is critical: a write path using "order:" + orderId and a read path using "orders:" + orderId never share cache entries, causing permanent 0% hit rate that is invisible until someone compares key naming conventions in code review. On write, delete the cache key rather than updating it. Race scenario with update: T1 reads DB (stale value) → T2 writes DB and updates cache → T1 writes its stale value to cache, overwriting T2's fresh entry. Deleting on write ensures any concurrent reader who misses will fetch from the DB post-commit, always getting the current value.

Write-Through and Write-Behind

A production write-behind incident: a fintech company cached user balance writes immediately and flushed to PostgreSQL every 500ms via a background worker. During a 90-second DB maintenance window, the flush queue accumulated 2.3 million pending writes. When the DB came back, the flush worker processed the queue sequentially over 8 minutes. During that window, 12 users who had initiated transfers saw inconsistent states — cache showed the post-transfer balance, DB showed the pre-transfer balance. For any crash during the 8-minute catchup, those writes would have been permanently lost. Write-Behind is appropriate for non-critical metrics: page view counters, analytics events, rate-limit counters — data where losing a few seconds of writes is acceptable. It must never be used for financial state, inventory, or any data requiring ACID guarantees. Production insight from Redis in Action: Carlson demonstrates write-behind for page-view analytics using Redis LPUSH to accumulate counters and periodic LRANGE+LTRIM flushes to the database — an explicit acknowledgment that the pattern trades durability for throughput, making it suitable only for data whose loss is "acceptable". For the write-through alternative, Carlson shows the Cache-Aside pattern for database row caching, noting that the cache serves as a transparent read-through layer that "can reduce page load time and database load" by eliminating repeated DB reads — the key correctness invariant being that every write hits the DB synchronously before the cache is updated.

Cache Stampede

Cache stampede mathematics: N concurrent requests arrive at TTL expiry. Each queries the DB independently, taking T_db seconds each. At 200 featured products × 500 concurrent users = 100,000 simultaneous DB queries. PostgreSQL max_connections=200 means 99,800 are queued or rejected with "too many connections." Probabilistic Early Expiration (PER): should_refresh = (now - item.set_time + beta * log(rand())) > ttl. With beta=1, some requests begin refreshing 2-3 log(N) seconds before TTL — proactively warming the cache before mass expiry. Implementation: store the item's set-time alongside the value (as a Hash field or JSON field) and check PER on each read. Distributed mutex: SET lock:{key} 1 NX PX 5000. Only one request populates cache; others either block briefly or serve stale data if available. Prefer serving stale over triggering a DB query avalanche — a 1-second-old product description is almost always better than a 503 error.

Key Takeaways

  • Cache-Aside for read-heavy workloads. Write-Through for write consistency. Write-Behind for write performance.
  • Cache stampede is a real production risk — mitigate with distributed locks or probabilistic early expiry.
  • Never cache all data — cache only hot data. Monitor hit rate to tune TTL and capacity.

Code example

// Cache-Aside implementation
class OrderService {
  Order getOrder(String orderId) {
    String key = "order:" + orderId;

    // 1. Check cache
    String cached = redis.get(key);
    if (cached != null) return deserialize(cached);

    // 2. Cache miss: read from DB
    Order order = orderRepository.findById(orderId);

    // 3. Populate cache with TTL
    redis.setex(key, 300, serialize(order));  // 5 min TTL

    return order;
  }

  void updateOrder(Order order) {
    orderRepository.save(order);
    // Invalidate cache — don't update, to avoid stale-read race
    redis.del("order:" + order.getId());
  }
}