hard Cache Write Strategies: write-through, write-back, write-around
A write strategy is two decisions: does the second store get written, and is it synchronous
Every cache-write strategy answers the same pair of questions: when the app writes, does the write touch the cache at all, and if it touches both cache and DB, does the second write happen synchronously (before the caller gets an ack) or asynchronously (after)? Four well-known strategies fall directly out of those two axes — write-through, write-back, write-around, and cache-aside — and every consistency/latency/durability trade-off below is a consequence of which answer you picked.
Write-through — write both, synchronously, before the ack
The app writes to the cache; the cache forwards the write to the database synchronously and only acks the app once the DB confirms. There is never a window after the ack where cache and DB disagree.
Traced — updating user:42.name from “Sam” to “Samir”:
| t (ms) | Event |
|---|---|
| 0 | App calls cache.put(user:42, {name:"Samir"}) |
| 0–12 | Cache forwards the write to the DB and blocks waiting for its commit |
| 12 | DB commits, acks the cache; cache commits its own copy and acks the app |
| 13 | A read for user:42 hits the cache and returns “Samir” — read-after-write is guaranteed fresh |
Total write latency = cache write + DB write; the cache never speeds up a write, only reads after it. Good fit when reads follow writes almost immediately and staleness is unacceptable (profile pages, account settings, inventory counts read right after checkout).
Write-back / write-behind — ack fast, flush later, batched
The app writes to the cache and gets an immediate ack; the cache buffers the write and flushes it to the DB asynchronously, often batching several writes to the same key into one DB write (write coalescing). This is the fastest write path, but it opens a durability window: the DB does not have the new value until the flush runs.
Traced — three writes to key counter:X inside one flush window:
| t (ms) | Event | DB state |
|---|---|---|
| 0 | Write A; cache acks in <1 ms | stale (old value) |
| 5 | Write B (same key); cache acks in <1 ms | stale |
| 9 | Write C (same key); cache acks in <1 ms | stale |
| 10 | Flush timer fires: only C (the latest value) is written to the DB — A and B never touch the DB individually | now = C |
Three writes coalesced into one DB write — that’s the throughput win. But if the cache node crashes at t=7 (after acking A and B, before the t=10 flush), both buffered writes are lost forever — the DB never sees them, and the app already told the user they succeeded. That is the defining risk of write-back: an ack is not a durability guarantee until the flush has actually happened. Replicating the cache buffer reduces the blast radius but does not remove the window.
Write-around — write DB only, bypass the cache entirely
The write goes straight to the DB; the cache is not touched, populated, or invalidated. This avoids filling the cache with data that is written once and rarely (or never) read again — e.g. an audit log row, a bulk import, a cold historical record.
Traced — write order:9001.status = "shipped", then a read arrives:
| t (ms) | Event |
|---|---|
| 0 | App writes directly to the DB; cache is never contacted |
| 8 | DB commits, acks the app |
| 9 | A read for order:9001 arrives — cache miss (it was never populated), falls through to the DB, loads the row, populates the cache |
| 10 | Every subsequent read hits the cache |
The cost is explicit and guaranteed: the very next read after a write-around write is a miss, paying a full DB round trip. That is the correct trade for data that’s written far more often than it’s read; it is the wrong choice for data that is read again soon after being written.
Cache-aside (lazy loading) — the default in most systems
Read path: the app checks the cache first; on a miss it loads from the DB and populates the cache itself (the cache never loads data on its own). Write path: the app writes to the DB, then either invalidates (deletes) the cache entry or updates it in place — the cache is never the primary target of a write.
Invalidate vs. update — not a cosmetic choice
Invalidate (delete) is the safer default: after the write, the key is simply absent, so the next reader is forced through the normal miss → load-from-DB → populate path and can never see a value older than the write. Update in place (write the new value straight into the cache right after the DB write) saves that next reader a DB round trip — but it reopens a race that invalidate mostly avoids, because it assumes no other read is concurrently in flight for the same key.
The concurrent-read-repopulates-stale race
Even with invalidate, a slow concurrent read can still leave the cache stale. Walk it with real timestamps:
- t0 — Reader R1 misses the cache for key K and begins a slow DB read (replica lag, a long query, GC pause — anything that takes a while).
- t1 — Writer W writes the new value to the DB, then invalidates (deletes) cache key K. K wasn’t cached yet, so the delete is a no-op.
- t2 — R1’s slow read finally returns — but it fetched the value as of before t1, i.e. the OLD value.
- t3 — R1, unaware anything changed, populates the cache with that old value. The cache now holds stale data, and there is no pending write left to invalidate it again.
This is why “invalidate on write” is a strong mitigation, not a proof of correctness: a read that started before the write finished can still complete after it and clobber the cache with old data. Mitigations: a short TTL as a backstop (bounds how long the staleness can last), or a version/timestamp check before repopulating (only write to cache if the fetched version is still the latest known).
Pitfalls
- Write-back data loss is invisible until the crash. The app already told the user the write succeeded; if the cache node dies before the flush, that acknowledgment was a lie. Only use write-back where the data is reconstructible or the loss window is genuinely tolerable (metrics, counters, ephemeral state) — not for money or orders.
- Treating write-around as “free”. If the written keys turn out to be read again soon (a wrong assumption about access pattern), write-around just adds a guaranteed miss on top of the DB write — worse than doing nothing. Confirm the write-once-read-rarely assumption before picking it.
- “Update on write” silently becomes the default and nobody notices the race. Update-in-place looks strictly better (saves a DB round trip) until the concurrent-read race above corrupts a cache entry with no obvious cause — it looks like "random" stale reads that are hard to reproduce.
- Invalidating a hot key reproduces a cache-stampede. The read right after an invalidate is, functionally, the same event as a TTL expiry: every concurrent reader now misses at once and can all hit the DB simultaneously. See Cache Stampede: single-flight, soft TTL & dedup — apply the same single-flight/lock fix to the post-invalidate miss on hot keys.
Judgment — which strategy for which workload
| Workload | Pick | Why |
|---|---|---|
| Read-heavy, reads follow writes (read-after-write matters) | Write-through, or cache-aside with invalidate | Cache is never allowed to be stale after an ack (write-through), or the very next read is forced to reload fresh (cache-aside + invalidate) |
| Write-heavy, bursts of writes to the same keys, brief staleness tolerable | Write-back | Coalescing collapses N writes into 1 DB write; you are trading a durability window for throughput |
| Write-once, rarely (or never) re-read | Write-around | Never pollute the cache with data unlikely to be requested again; pay the one guaranteed miss only if it is actually re-read |
| General-purpose, mixed read/write, want lazy population | Cache-aside | Only caches what is actually read (cold data never enters the cache); most flexible default |
Invalidate vs. update, restated as a trade-off: invalidate costs one extra DB round trip on the next read but bounds staleness to “never” in the common case; update-in-place saves that round trip but must be paired with a version check or short TTL if any concurrent-read race is possible — without one, it is not actually safer than doing nothing.
Cache-aside vs. write-through, as named alternatives: write-through guarantees freshness on every write but pays write latency on every write, even for keys that are never read again; cache-aside only pays a load cost for keys that are actually requested, but has a small window (above) where a stale value can be repopulated. Pick write-through when staleness is unacceptable and writes are far less frequent than reads; pick cache-aside when most written keys are read again soon but not so urgently that the race above matters.
Takeaways
- Every write strategy reduces to two choices: does the write touch the cache at all, and is the second store written synchronously or asynchronously — every trade-off follows from that pair.
- Write-through and cache-aside(invalidate) keep cache and DB consistent by construction, at the cost of write latency (write-through) or a guaranteed next-read miss (write-around is the extreme version of avoiding the cache entirely).
- Write-back buys the fastest writes and write coalescing at the cost of a durability window — a crashed cache node loses un-flushed writes.
- Cache-aside’s invalidate-vs-update choice is a real fork: invalidate is safer but not race-free — a slow concurrent read can still repopulate a stale value after the write; back it with a short TTL or a version check.
Sources: Kleppmann, "Designing Data-Intensive Applications" (ch. 3, 11); AWS "Database Caching Strategies Using Redis" whitepaper; Redis and Memcached documentation on write-through/write-behind; ByteByteGo and Hello Interview system-design caching notes. See also: Cache Stampede & Invalidation, ReadThrough vs WriteThrough Cache, Cache Invalidation, Consistent Hashing. Re-authored/Deepened for this guide.
🤖 Don't fully get this? Learn it with Claude
Stuck on Cache Write Strategies: write-through, write-back, write-around? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Cache Write Strategies: write-through, write-back, write-around** (System Design) and want to truly understand it. Explain Cache Write Strategies: write-through, write-back, write-around from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Cache Write Strategies: write-through, write-back, write-around** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Cache Write Strategies: write-through, write-back, write-around** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Cache Write Strategies: write-through, write-back, write-around** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.