awesome-everything RU
↑ Back to the climb

Caching

Cache invalidation: code and snippet reading

Crux Read real cache-aside snippets — a key-design bug, a purge that races a reader, and a fixed-TTL stampede — and pick the highest-leverage fix a senior makes first.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

Invalidation bugs hide in the cache-key string and the order of two lines on the write path. Read each snippet, find the staleness gap it opens, and choose the fix a senior engineer would make first.

Goal

Practise the loop you run in every cache-consistency incident: read the key design and the write path, predict where stale data leaks in, and reach for the highest-leverage fix — not the biggest hammer.

Snippet 1 — the cache key

// GET /products?category=shoes&sort=price&page=2&utm_source=newsletter
function cacheKey(req) {
  return "products:" + req.url;           // whole URL, query string and all
}

async function handler(req, res) {
  const k = cacheKey(req);
  let body = await redis.get(k);
  if (!body) {
    body = await db.queryProducts(req.query);
    await redis.set(k, body, "EX", 300);
  }
  res.send(body);
}
Quiz

The product catalogue rarely changes, yet hit rate is near zero and the origin is busy. What is wrong with this key, and what is the fix?

Snippet 2 — the purge on write

async function updateUser(id, patch) {
  const row = await db.users.update(id, patch);   // DB is now fresh
  await redis.del("user:" + id);                   // drop the cache entry
  return row;
}

// elsewhere, the read path (cache-aside):
async function getUser(id) {
  const k = "user:" + id;
  let u = await redis.get(k);
  if (!u) {
    u = await db.users.find(id);                   // may read a pre-update row
    await redis.set(k, u, "EX", 3600);             // ...and write it back
  }
  return u;
}
Quiz

This passes every local test but reverts user edits in production for up to an hour. What is the defect and the cheapest production patch?

Snippet 3 — the TTL

const TTL = 600; // 10 minutes, same for every entry

async function cacheConfig(tenantId) {
  const k = "config:" + tenantId;
  let cfg = await redis.get(k);
  if (!cfg) {
    cfg = await db.loadConfig(tenantId);    // expensive: ~400ms, joins 5 tables
    await redis.set(k, cfg, "EX", TTL);
  }
  return cfg;
}
Quiz

After a fleet-wide deploy that warms many tenants' configs at once, latency p99 spikes hard every 10 minutes. What is happening and the first fix?

Recap

Three classic invalidation bugs, all read straight from the code: a cache key that folds in irrelevant params shatters hit rate, so build keys from only the response-affecting params, normalised; delete-on-write races a concurrent reader’s repopulating SET, patched cheapest with delayed double-delete (leases or write-through for the strong fix); and a fixed TTL warmed in one window stampedes the origin on every expiry, defused with jitter plus single-flight. Read the key and the write path first, name the gap, then pick the smallest fix that closes it.

Continue the climb ↑Cache invalidation: reproduce and close the race
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.