Core Concept

Caching Patterns and Invalidation

A cache is a copy of hot data in fast memory; the hard part is invalidation — when and how to drop or refresh entries when the database changes.


1. What It Is

Caching is the fastest lever in system design — and the easiest to get wrong. We add a cache when read QPS or latency demands it, then pick an invalidation strategy before drawing the Redis box.

What:

An in-memory layer (Redis, Memcached) that sits between your app and the database. Reads check the cache first and only hit disk on a miss.

Primary purpose:

Cut read latency from milliseconds to microseconds and keep the database from becoming the bottleneck on hot keys.

Usually used for:

Sessions, precomputed feeds, rate limit counters, and any key that gets read far more often than it gets written.

2. Core Mental Model

Start with one rule: the cache is a copy, not the source of truth. If Redis disappears, you rebuild from the database. Everything else follows from how reads and writes interact with that copy.

🔋 Cache is ephemeral

Never store data you cannot recreate. The database (or object store) owns durability; the cache owns speed.

🎯 Cache-aside (lazy load)

Read: check cache, on miss load from DB and populate. Write: update DB, then delete the cache key so the next read gets fresh data.

🔀 Write-behind

Acknowledge the write in memory immediately and flush to the database in background batches. Fast writes, but you can lose recent data if the cache node crashes.

In the room

Saying "we'll cache everything" without an invalidation story is a common stumble. State cache-aside vs write-through, name your TTL, and explain what happens on a write. If they ask about thundering herd, mention singleflight or request coalescing.

3. Why It Matters in HLD

Caching is the first lever on read-heavy designs — but only if we have an invalidation story. Three lenses:

Needed When:

Read-to-write ratios are massive (e.g. 99:1), database disk I/O bottlenecks, or sub-millisecond latencies are strict SLAs.

Avoids:

Database CPU exhaustion, slow point-lookup sequential scans, expensive duplicate calculations, and high cloud database hosting costs.

Optimizes For:

Sub-millisecond query response speeds, scalable read concurrency, and backend resource preservation.

4. Architecture & Data Flow

Walk the cache stack top-down as interview steps. Step 1 — Browser/CDN: cache static and cacheable GETs at the edge. Step 2 — App check: read path hits Redis before the database. Step 3 — Miss path: load from DB, populate cache with TTL. Step 4 — Write path: update DB, then delete or update cache key — state cache-aside vs write-through. Step 5 — Stampede guard: mention singleflight or probabilistic early refresh for hot keys.

Loading...

Core Caching Patterns Code Layout

Standard lazy-load Cache-Aside query and update paths executed in code:

Read:
  data = cache.get(key)
  if data is None:
      data = db.query(key)
      cache.set(key, data, ttl=300)
  return data

Write:
  db.update(key, data)
  cache.delete(key)  // Invalidate immediately to prevent stales

5. Key Characteristics

Pattern choice determines staleness vs write latency — we walk the table row by row:

  • Lazy vs Aggressive Loading: Cache-Aside loads on demand; Write-Through preempts misses at the cost of write speeds.
  • Read and write paths differ — cache-aside, write-through, and write-behind each trade staleness against write latency:
PatternProsCons
Cache-Aside (Lazy)
  • Cache contains only active hot keys
  • simple to code
  • node crash is degraded, not fatal
  • Cache miss penalty on first read
  • data can fall stale if not proactively deleted
Write-Through
  • Zero cache staleness
  • highly consistent
  • ideal for read-heavy key sets
Write latency increases (must write synchronously to both cache and database)
Write-Behind (Back)
  • Ultra-low write latency
  • DB write batching offloads high transactional disk I/O
Immediate risk of data loss if the in-memory cache nodes crash before syncing to disk
  • Eviction policies — when memory fills, the cache must drop keys:
PolicyEvictsPrimary Use Case
LRU (Least Recently)Evicts items that have not been read for the longest timeStandard general-purpose caching (Redis allkeys-lru default)
LFU (Least Frequently)Evicts items with the lowest access frequency counterIdeal for static assets that must maintain permanent popularity
TTL (Time To Live)Evicts keys immediately upon expiration of absolute epoch durationEphemerals, shopping carts, session tokens, or OTP entries

In the room

If they ask about thundering herd, say singleflight or XFetch — not "we'll add more Redis." Invalidation strategy matters as much as the cache box on the diagram.

6. Strategic Tradeoffs

Cache-aside, write-through, and write-behind each optimize a different axis — we compare openly:

Cache-aside minimizes wasted memory but allows brief staleness; write-through maximizes consistency at the cost of write latency; write-behind maximizes write throughput but risks loss on crash.

7. Failure / Bottleneck Awareness

Thundering herd, cache penetration, and avalanche are the classic interview probes — we lead with mitigations:

🐂 The Cache Stampede (Thundering Herd)

Problem: A hot key like homepage_feed expires and hundreds of threads miss at once, each querying the database for the same row.

Mitigation: Use a lock so only one thread rebuilds the key (singleflight), or probabilistic early refresh (XFetch) to recompute before expiry.

🕳️ Cache Penetration (Malicious UUID Misses)

Problem: Attackers request random keys that do not exist. Every request misses the cache and hits the database.

Mitigation: Bloom filter at the gateway to reject unknown keys, or cache null results with a short TTL (30s).

❄️ Cache Avalanche (Staggered TTLs)

Problem: Many keys get the same TTL at boot time and expire together, causing a synchronized DB spike.

Mitigation: Add random jitter to TTLs (e.g. base + random 0–30s) so expirations spread out.

8. Common HLD Usage

Invalidation strategy follows from how stale the product can tolerate — match strategy to use case:

Invalidation StrategyAction PathBest Case Use
TTL-BasedAttach absolute expiration time window to cache keysGeneral session data
Event-DrivenListen to CDC db change event → publish invalidate command to RedisStrong consistency
Key VersioningAppend static version strings to route keys: user:100:v2Safe zero-lock rollouts

9. Decision Signals

Add a cache when read QPS dominates and hot keys follow a Zipfian distribution:

🎯 Think Caching When:
  • Your system design workload has highly unequal distribution (e.g., Pareto Zipfian 80/20 read hotspots).
  • You are designing heavy read platforms like social feeds, user profiles, or product inventories.
  • You face high point-lookup speeds that must return in sub-5 milliseconds.

11. Deep Dive (Optional)

The XFetch Probabilistic Cache Expiration Algorithm

To eliminate the Cache Stampede entirely without blocking thread pools, high-scale systems deploy **XFetch**. Instead of waiting for a key to expire to trigger recomputation, readers evaluate a probabilistic test during normal gets:

PYTHON
# Probabilistic early compute check
import random, math

def should_recompute(remaining_ttl, compute_time, beta=1.0):
    # beta > 1.0 makes early recomputation more aggressive
    return (remaining_ttl - (compute_time * beta * math.log(random.random()))) < 0

If the test evaluates to `True`, the reader returns the current cached value immediately, but asynchronously kicks off a background thread to refresh the cache key from the database.

Cache Consistency Race Conditions

Under high concurrency, the Cache-Aside pattern (write: update DB, delete cache) has a rare but catastrophic race condition:

  1. Cache is empty. Client A queries key $K$; misses; queries DB and gets value $V1$.
  2. Client B updates key $K$ in DB to $V2$; deletes cache key (currently empty anyway).
  3. Client A (delayed by CPU scheduling) writes stale value $V1$ back into the cache.
  4. The cache now stores stale value $V1$ indefinitely until the next TTL or write.

Mitigation: Enforce short TTL values as a safety net, or use transactional locking layers (e.g., MySQL locks or Redis locks) during cache recomputation.

Multi-Layer Cache Stack

Caching is not only "Redis in front of Postgres." Production systems stack layers — each with different TTL, invalidation cost, and hit ratio:

  1. Client / browser cache: HTTP Cache-Control headers for static assets and idempotent GET responses — zero server load on repeat visits.
  2. CDN edge: Geo-distributed cache for images, video segments, and public API GETs with short TTL — see CDN & Edge Delivery.
  3. In-process (local) cache: Caffeine/Guava map inside each app pod — microsecond reads for config and hot keys; invalidated via pub/sub on change.
  4. Distributed cache (Redis/Memcached): Shared across pods; survives pod restarts; handles hot keys with replication or key splitting.
  5. Database buffer pool: OS/page cache and InnoDB buffer — last line before disk; not a substitute for application-level design.

Interview signal: walk the read path top-down — "CDN miss → Redis miss → read replica → primary" — and state where each layer adds value vs complexity.

Do not cache everything — profile read paths first and cache only keys with high read frequency and tolerable staleness. Caching data that changes every request adds latency without benefit.

Hot Key Mitigation

A viral post or celebrity profile concentrates reads on one cache key. A single Redis node serving that key becomes a bottleneck. Mitigations: replicate the hot key across Redis nodes with random read selection, local in-process caching on app servers for ultra-hot keys, or pre-warm CDN/edge caches before traffic spikes.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...