Skip to content
$EngineeringAtlas

Caching Strategies and the Invalidation Trap

Cache-aside, write-through, and write-behind — what each one guarantees, where they break, and how to think about the hardest part: invalidation.

Amit Kumar Singh3 min read

The Problem

A read-heavy endpoint hammers your database with the same query thousands of times a second. The obvious fix is a cache. The non-obvious part is everything that comes after: stale data, thundering herds, and the question that has haunted engineers forever — when do you invalidate?

Why It Matters

Caching is the highest-leverage performance tool you have, but it trades freshness for speed. Get the strategy wrong and you serve stale prices, leak one user's data to another, or melt your database the moment a popular key expires.

Core Concepts

Three patterns cover most needs:

  • Cache-aside (lazy): the app checks the cache, and on a miss loads from the database and populates it. Simple, and the default for most systems.
  • Write-through: writes go to the cache and the database together, so the cache is always warm and consistent — at the cost of slower writes.
  • Write-behind: writes hit the cache and are flushed to the database asynchronously. Fast writes, but a crash can lose buffered data.

Implementation

Cache-aside is the workhorse:

async function getUser(id: string): Promise<User> {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findUnique({ where: { id } });
  // Short TTL bounds staleness even if an invalidation is missed.
  await redis.set(key, JSON.stringify(user), "EX", 300);
  return user;
}

async function updateUser(id: string, data: Partial<User>) {
  const user = await db.users.update({ where: { id }, data });
  await redis.del(`user:${id}`); // invalidate, don't try to update in place
}

Deleting on write is safer than rewriting the cache: the next read repopulates from the source of truth, so you can't cache a half-applied update.

Common Mistakes

  • No TTL. Without expiry, a single missed invalidation means data that's stale forever. A TTL is your safety net.
  • Caching per-user data under a shared key. The fastest way to leak one user's data to another. Always namespace by identity.
  • Updating the cache in place on writes. Race conditions can leave the cache newer-but-wrong. Delete and let the next read rebuild it.

Production Considerations

Guard against cache stampedes: when a hot key expires, thousands of concurrent misses all hit the database at once. Mitigate with a short lock so one request recomputes while others wait, or by refreshing slightly before expiry.

const lock = await redis.set(`lock:${key}`, "1", "NX", "EX", 5);
if (!lock) return await waitForFreshValue(key); // someone else is recomputing

Security

Never let a cache key be derived from unsanitized user input in a way that crosses tenant boundaries. Treat cached responses as carrying the same authorization context as the original read.

Performance

The win is dramatic — sub-millisecond reads instead of database round-trips — but measure hit rate. A cache below ~80% hit rate often adds latency and complexity for little gain. Size it so the working set actually fits.

Summary

Pick cache-aside unless you have a specific reason not to. Always set a TTL, delete rather than rewrite on updates, namespace keys by identity, and plan for stampedes on hot keys. Caching is easy to add and hard to get exactly right — the TTL is what saves you when invalidation inevitably slips.

Amit Kumar Singh

// written by

Amit Kumar Singh

Software engineer writing about backend systems, cloud, and the realities of running code in production.

$ subscribe --weekly

The weekly engineering digest

Production-grade engineering writing in your inbox. No spam, unsubscribe anytime.

## related

[Backend]▲ trending

Designing Idempotent APIs That Survive Retries

Networks fail, clients retry, and duplicate requests happen. Here's how to design write endpoints that produce the same result no matter how many times they're called.

Amit Kumar Singh3 min read