Caching strategies: cache-aside, write-through, and write-behind
Three ways to put a cache next to a database, what each one does when a write lands, and the failure each one is quietly signing you up for. With the invalidation problem stated honestly.
A cache is the cheapest performance win in any architecture and the most common source of bugs that only reproduce in production. Both facts come from the same place: a cache is a second copy of your data, and a second copy is something that can disagree with the first.
There are three patterns worth knowing by name. They differ in exactly one respect — what happens on a write — and each one buys speed with a different kind of risk. Knowing which risk you just accepted is the difference between "I'd put Redis in front of it" and an answer that survives a follow-up.
Cache-aside
The default, and what people usually mean when they say "we added a cache". The application talks to both stores and owns the coordination.
On a read: look in the cache, and on a miss read the database and put the result back. On a write: write the database, then delete the cached key.
async function getPaste(id) {
const hit = await redis.get(`paste:${id}`);
if (hit) return JSON.parse(hit);
const row = await db.paste.findUnique({ where: { id } });
if (row) await redis.set(`paste:${id}`, JSON.stringify(row), { EX: 3600 });
return row;
}
async function updatePaste(id, patch) {
const row = await db.paste.update({ where: { id }, data: patch });
await redis.del(`paste:${id}`); // delete, not overwrite — see below
return row;
}
Why delete rather than update. Writing the new value into the cache looks tidier and is a race waiting to happen: two concurrent writers can interleave so that the newer database value is followed into the cache by the older one, and the cache then serves stale data until the TTL expires. Deleting is idempotent — the worst outcome of a duplicated delete is one extra miss.
What it gets right. The database stays the source of truth. A cache outage degrades you to slow rather than broken. Only data anybody actually asked for ever occupies memory.
What it costs you. Every miss pays both round trips. There is a window between the database write and the cache delete where readers get the old value. And it has one genuinely nasty failure mode:
Read-through is the same read path with the bookkeeping moved into a library or
the cache itself, so the application just calls get. Same trade-offs, less code
in your handlers, and the cache now needs to know how to load from your database.
Write-through
The write goes to the cache, and the cache writes it onward to the database before acknowledging.
What it gets right. The cache is never stale, because nothing reaches the database without passing through it. Read-after-write does the obvious thing — you read what you just wrote — and that alone kills a large family of bugs.
What it costs you. Every write now pays both hops before it can be acknowledged, so write latency goes up. And unless you pair it with a TTL, you fill memory with data that may never be read: write-through caches everything written, not everything wanted. On a write-heavy workload with cold reads, it is strictly worse than cache-aside.
Reach for it when writes are rare relative to reads and reading your own write immediately actually matters — a user profile, a settings blob, a document that one person edits and then looks at.
Write-behind
Acknowledge the write from the cache immediately, and flush to the database asynchronously — usually batched.
What it gets right. Writes are as fast as memory. Better, batching collapses work: a counter hit ten thousand times in a minute becomes one database update instead of ten thousand, which is the difference between a feasible design and an impossible one.
What it costs you. There is a window — usually seconds — in which the write has been acknowledged to the user and exists only in the cache. If that node dies, the data is gone, and you told the user it was saved. Replication and an append-only log narrow the window; nothing closes it.
That is a real trade and sometimes an obviously correct one. View counts, "last seen" timestamps, analytics events, likes: losing three seconds of them costs nothing and the write reduction is enormous. Payments and orders: never.
Which one, in one table
| Write path | Consistency | Fails by | |
|---|---|---|---|
| Cache-aside | DB, then invalidate | Brief staleness after a write | Stampedes; stale window |
| Write-through | Cache to DB, then ack | Strong for cached keys | Slow writes; memory waste |
| Write-behind | Cache acks, DB later | Eventual | Data loss on node failure |
Most real systems are cache-aside, with write-behind on specific high-volume, low-value counters. If you say that in an interview you are describing what most production systems actually do.
The part that is actually hard
Choosing a pattern is the easy half. Invalidation is the half that generates incidents, and there are only three real tools:
TTL. Simple, bounded, and always correct in the limit — every key is at most one TTL stale. Add jitter. This handles more than people expect, and if the staleness bound is acceptable it is the whole answer.
Explicit invalidation. Delete on write. Precise, and it fails when the delete fails — a dropped connection between the database commit and the delete leaves a stale key until its TTL. Which is why you keep the TTL even when you invalidate explicitly: it is the backstop.
Versioned keys. Put a version in the key — paste:v7:123 — and a write bumps
the version. Nothing is deleted; the old key ages out on its own. This is the one
that survives multi-key and derived-data invalidation, where "which keys does
this write affect?" has no simple answer.
Saying it well
The strongest version of a caching answer has four parts and takes about thirty seconds: what you cache, which pattern, what the TTL is, and what goes stale. "I'd cache the rendered timeline per user, cache-aside, 60-second TTL with jitter, so a new post can take up to a minute to appear for followers — and that's acceptable because it already does on every real feed."
That last clause is the one that separates candidates. Anybody can add a cache. The job is knowing what you just broke, and being ready to defend it.
Now design it yourself
Draw the architecture on a board and have it graded against the things an interviewer pushes on. Free, and no account needed.
- Design a distributed cacheDesign the cache itself, not a system that uses one: a fleet of in-memory nodes that other services get and set keys against. Persistence to disk is out of scope.7 checks
- Design a photo feedDesign the home feed for a photo-sharing app: users post images and see a feed of posts from the people they follow. Assume some accounts have very large follower counts.9 checks
- Design a view counterDesign the thing that counts views on an article or a video and shows the count back to the reader. Per-user analytics and charts are out of scope.7 checks