Module F-12·15 min read

The four caching strategies with their consistency guarantees, failure modes, and implementation trade-offs. Cache stampede prevention, invalidation timing, cache warm-up, and the decision framework for choosing the right pattern.

Written by Jatin Jain Saraf · Architect of SupraScan

F-12 — Caching Patterns: Cache-Aside, Write-Through, Write-Behind, and Read-Through

Who this module is for: You use Redis as a cache — you call redis.get(), fall back to the database on miss, and call redis.set(). That is the cache-aside pattern, and it is correct for many situations. But it is not the only pattern, and it is not always the right one. This module covers the four caching strategies with their trade-offs, consistency implications, and the failure modes most tutorials skip.


What a Cache Actually Does

A cache sits between your application and your primary data store (typically a database). Its job is to answer repeated, expensive lookups cheaply by storing previously computed or fetched results in fast memory.

Every caching strategy makes a trade-off across three dimensions:

  1. Consistency — how closely the cache matches the source of truth at any given moment
  2. Complexity — how much application code is required to maintain the cache
  3. Failure behavior — what happens when the cache is empty, stale, or unavailable

Pattern 1: Cache-Aside (Lazy Loading)

Also called "lazy loading" or "look-aside cache." This is what most engineers mean when they say "we cached it in Redis."

How It Works

Application                     Redis                    Database
    │                              │                         │
    │──── GET user:1001 ──────────►│                         │
    │◄─── (nil) ──────────────────│                         │
    │                              │                         │
    │──── SELECT * FROM users ─────────────────────────────►│
    │◄─── {id:1001, name:...} ─────────────────────────────│
    │                              │                         │
    │──── SET user:1001 {..} EX 300►│                        │
    │                              │                         │
    │    [next request]            │                         │
    │──── GET user:1001 ──────────►│                         │
    │◄─── {id:1001, name:...} ────│                         │

Read path:

  1. Try GET key from Redis
  2. On hit: return the cached value
  3. On miss: query the database, store the result in Redis with a TTL, return the value

Write path: Write directly to the database. Optionally invalidate (delete) the cache key. Do not write to the cache on writes.

async function getUser(userId: string) {
  const key = `user:${userId}`;
 
  // 1. Try cache
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  // 2. Cache miss — query database
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  if (!user) return null;
 
  // 3. Populate cache with TTL
  await redis.set(key, JSON.stringify(user), 'EX', 300);
  return user;
}
 
async function updateUser(userId: string, data: Partial<User>) {
  // Write to database
  await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
 
  // Invalidate cache — next read will re-populate from DB
  await redis.del(`user:${userId}`);
}

When to Use Cache-Aside

  • Read-heavy workloads where most reads are for data that rarely changes
  • Resilient to cache failures — if Redis goes down, the application still works (just slower)
  • Infrequent writes — cache invalidation on every write is cheap

Trade-offs and Failure Modes

Cache stampede (thundering herd): If a popular key expires, many concurrent requests simultaneously see a miss and all rush to the database. The database receives N copies of the same expensive query. Solutions:

  • Probabilistic early expiry (PER): Recompute when TTL < threshold × random(). Some requests recompute before the key expires, warming the cache before the stampede.
  • Mutex / distributed lock: The first miss acquires a lock, computes, and populates the cache. Others wait and then read from the cache.
async function getUserWithLock(userId: string) {
  const key = `user:${userId}`;
  const lockKey = `lock:user:${userId}`;
 
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  // Try to acquire lock (NX = only if not exists, EX = expire in 5s)
  const locked = await redis.set(lockKey, '1', 'NX', 'EX', 5);
 
  if (locked) {
    // We have the lock — compute and cache
    const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
    await redis.set(key, JSON.stringify(user), 'EX', 300);
    await redis.del(lockKey);
    return user;
  } else {
    // Another process is computing — wait briefly and retry
    await sleep(50);
    return getUserWithLock(userId);
  }
}

Stale data: After an invalidation (DEL), the next request fetches fresh data from the database. But if you forget to invalidate after a write, the cache serves stale data until TTL expires.

Cache miss on cold start: After a deployment or Redis restart, the cache is empty. All requests hit the database simultaneously. Mitigate with cache warming (pre-populate common keys on startup).


Pattern 2: Write-Through

Write-through keeps the cache synchronously updated on every write. The application writes to the cache and the database together — the write is only acknowledged when both succeed.

How It Works

Application                     Redis                    Database
    │                              │                         │
    │──── Write user:1001 ────────►│                         │
    │     (SET user:1001 {...})     │                         │
    │                              │──── UPDATE users ───────►│
    │                              │◄─── OK ─────────────────│
    │◄─── OK ─────────────────────│                         │

Read path: Same as cache-aside — try cache, fall back to database.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.