Core Concepts

Caching

Trading a little staleness for a lot of speed — where caches live, the cache-aside pattern, invalidation strategies, the stampede problem, and deciding what is allowed to be wrong for a while.


A cache keeps a copy of expensive-to-compute data somewhere fast, so most requests skip the expensive work. It's the single highest-leverage performance tool you have — and the single easiest way to serve wrong data. The whole skill is deciding what may be stale, and for how long.

Caches live at every layer

"Add a cache" isn't one decision — there's a cache at nearly every hop between the user and your database. Cheapest wins are highest up:

LayerExampleCaches
BrowserHTTP Cache-ControlStatic assets, GET responses
CDNCloudflare, CloudFrontImages, public pages, API GETs
In-memory (app)A local mapConfig, hot lookups per instance
DistributedRedis, MemcachedShared results across all app servers
DatabaseQuery/buffer cacheHot rows and pages

Push caching as far up as you can: a response served from a CDN never touches your servers at all.

The cache-aside pattern

The default pattern for application data. Your code owns the logic:

read:   check cache → hit? return it
                    → miss? read DB, store in cache with a TTL, return it
write:  write DB, then delete (or update) the cache key
async function getProduct(id: string) {
  const key = `product:${id}`;
  const hit = await cache.get(key);
  if (hit) return JSON.parse(hit);
 
  const row = await db.product.findUnique({ where: { id } });
  await cache.set(key, JSON.stringify(row), { ex: 300 }); // 5 min TTL
  return row;
}

Two properties make this safe: a cache miss degrades (you just hit the DB, you don't fail), and a TTL guarantees staleness is bounded even if an invalidation is missed.

Invalidation is the hard part

"There are only two hard things in computer science: cache invalidation and naming things."

Three strategies, usually combined:

  • TTL (expiry): entries die after N seconds. Simple; data can be stale up to N.
  • Delete-on-write: after any write that affects a key, delete it so the next read recomputes. Fresher, but you must remember every key a write touches.
  • Versioned keys: bake a version into the key (v2:product:{id}) and bump it to discard a whole class of entries at once — handy on deploys or schema changes.

TTL + delete-on-write is the pragmatic default: the delete keeps it fresh, the TTL is a safety net for the invalidation you forgot.

Beware the stampede

When a hot key expires, thousands of concurrent requests all miss at once and hammer the database together — a "cache stampede" that can take the DB down precisely when traffic is highest. Defences:

  • Add jitter to TTLs so many keys don't expire on the same second.
  • Lock on rebuild: let only the first request recompute a missing hot key while the rest wait briefly and retry.
  • Pre-warm critical keys before traffic arrives.

Decide what may be stale — out loud

This is a product decision, not a technical one. Make the list explicit:

Cache hard (seconds–minutes)Cache briefly / carefullyNever cache
Follower counts, like countsPrices, inventory displayAccount balances
Public profilesSearch resultsStock at purchase time
Homepage / trending feedsA user's unread countAuth / permission checks

The through-line: a number people glance at can be a little wrong; a number people act on cannot. Showing a slightly stale seat map is fine; selling a seat based on a stale map is not.

What to say in an interview

Name the hot read, put a cache in front of it, and state the invalidation and the staleness you're accepting:

"The product page is read thousands of times a minute and changes rarely, so I'll cache it in Redis cache-aside with a 5-minute TTL, and delete the key on any product update. If a key is wrong for a few seconds it's fine — it's display data. I'd never cache the checkout stock count the same way; that reads from the primary inside the purchase transaction."


Next up: Sharding — what to do when the data no longer fits on one machine.