CMD Guide
HomeSystem DesignSystem Design Trade-offs

ReadThrough vs WriteThrough Cache

Read-through and write-through are not two choices for the same decision — read-through governs what happens on a cache miss (the cache library, not your application, loads the value from the backing store and populates itself), while write-through governs what happens on a write (the cache library writes through to the store synchronously and only then acknowledges). One is a read policy, the other a write policy; they sit on orthogonal axes, and a real caching layer picks one of each. Comparing them head-to-head is like comparing "how you get groceries in" against "how you restock the shelf" — related, but not rivals.

The genuine rivalries are within each axis. On reads, read-through competes with cache-aside (lazy loading). On writes, write-through competes with write-back and write-around. That is where the judgment lives, and where consistency and durability are actually traded.

The two axes, and who owns the logic

The defining question for a read policy is where the miss-handling code lives. With cache-aside, your application owns it: on a miss, your code queries the DB and writes the value back into the cache. With read-through, the cache sits inline as a provider — your code only ever calls cache.get(key), and the cache itself fetches-and-populates on a miss. Same physical data flow (App → Cache → DB → Cache → App); the difference is who wrote the code and therefore who owns the failure modes.

The write axis has three options, distinguished by when the store is updated relative to the ack:

diagram
diagram

Read-through vs cache-aside — the code tells the story

Cache-aside, written explicitly in your service:

def get_product(pid):
    key = f"product:{pid}"
    val = cache.get(key)              # 1. look in cache
    if val is not None:
        return val                    #    hit
    val = db.query_product(pid)       # 2. miss: app loads from DB
    cache.set(key, val, ttl=60)       # 3. app populates cache
    return val

Read-through deletes steps 2–3 from your code and moves them into the cache provider (a CacheLoader in Caffeine/Guava, a DataLoader/loader function in a read-through client). Your service just calls cache.get(key) and the loader runs on a miss.

Why the naive write path is wrong. A common bug when pairing a read cache with writes is to update the cache after updating the DB: db.update(...) ; cache.set(key, newval). Two concurrent writers can interleave so the cache ends up holding the older value permanently. The correct cache-aside write is update the DB, then invalidate (delete) the keydb.update(...) ; cache.delete(key) — so the next read re-loads fresh. Facebook's memcache team documented exactly this class of stale-set race. Write-through sidesteps it because the cache and store are updated in one ordered operation.

Invalidation shrinks the race window but does not close it. A reader that loaded the old DB value just before your write can still set it just after your delete, re-installing stale data until TTL. The interleaving: (1) reader misses on key K and reads the DB → old value; (2) writer updates the DB and deletes K; (3) the slow reader now executes cache.set(K, old) — the cache holds the old value while the DB holds the new one, and no further invalidation is coming. Facebook closes this with leases (the same NSDI 2013 memcache paper): a cache miss hands the reader a lease token, a delete invalidates outstanding tokens, and the cache rejects any set whose token has been invalidated — turning last-write-wins into first-writer-since-invalidation-wins. If your cache has no leases, your backstop is the TTL: choose it as the maximum staleness you can tolerate from exactly this race.

Worked trace: read-through + write-through on product:42

Redis in front of Postgres. Assume: cache read 0.5 ms, cache write 0.5 ms, DB read 8 ms, DB write 10 ms. TTL on cached entries = 60 s. A seller changes the price of product 42 from 799 to 899.

StepOperationWhat happensLatencyState after
1GET price(42)Cache miss. Read-through loader queries DB (799), populates key with TTL 60s.0.5 + 8 = 8.5 mscache: 799 (ttl 60s); db: 799
2GET price(42)Cache hit. Served from Redis.0.5 mscache: 799; db: 799
3SET price(42)=899Write-through: cache updated, then DB written synchronously, then ack.0.5 + 10 = 10.5 mscache: 899; db: 899
4GET price(42)Cache hit — already fresh, no miss, no DB touch.0.5 mscache: 899; db: 899

Now swap the write policy to write-back at step 3: the ack returns in 0.5 ms (10 ms saved per write), but the DB still holds 799 until the async flush fires ~200 ms later. Any process reading the DB directly in that window — a replica, an analytics job, a recovery after a cache crash — sees the stale 799. That gap is the durability/consistency price of write-back.

diagram
diagram

What happens on eviction and TTL

Every entry leaves the cache one of three ways, and the policy pairing determines whether that is safe:

Key consequence: read-through + write-through is clean under eviction because the cache never holds un-persisted state. Write-back makes eviction a correctness question, not just a performance one.

Pitfalls

When to use it / when NOT to — and the alternatives

Read axis: read-through vs cache-aside

Choose read-through when you want a single, uniform cache access path and are willing to centralize loader logic (and can rely on the cache library's coalescing/TTL). You gain simpler call sites and one place to fix miss behavior. It costs flexibility: the loader must be generic, you cannot easily fetch multiple keys in one DB round-trip, and you inherit the library's semantics. Prefer cache-aside when reads need custom logic (batching, joins, partial fields, per-request DB routing) or when you want the cache to be strictly optional so an app can still serve on a cache outage. Signal to reach for cache-aside: "the miss path is more than one query."

Write axis: write-through vs write-back vs write-around

Choose write-through when read-after-write consistency matters and reads immediately follow writes (user edits their own profile, price update that must show instantly), and write volume is modest enough to absorb store latency on every write. You gain a cache that is never stale and safe under eviction; you pay store latency and store load on every write, and couple write availability to the cache.

Prefer write-back when writes are the bottleneck and are bursty or repeatedly overwrite the same key (metrics, counters, leaderboards, session heartbeats). You gain low write latency and write coalescing (100 increments → 1 DB write); you pay a durability window and eviction-correctness complexity. Size that window before choosing it: if the buffer flushes every 50 ms and you are taking 20,000 writes/s, a cache-node crash loses up to 50 ms × 20,000/s = ~1,000 acknowledged writes — invisible for view counters, catastrophic for payments. That product (flush interval × write rate) is the concrete crossover that decides whether write-back is admissible. Never for data you cannot afford to lose.

Prefer write-around for write-once / read-rarely data (audit logs, event ingestion) where caching the write would only pollute the cache. You gain a clean cache; you pay a guaranteed miss on the first read of any freshly-written item.

The common default for a general read-heavy service is cache-aside reads + write-around (invalidate on write), because it keeps the cache optional and never traps un-persisted state. Read-through + write-through is the choice when you want the cache to be the authoritative, always-fresh access layer and can accept the write latency and the cache being on the critical path.

Takeaways


Sources: AWS Database Caching Strategies Using Redis whitepaper and Amazon ElastiCache docs (lazy loading / write-through / TTL); Nishtala et al., Scaling Memcache at Facebook, NSDI 2013 (stale-set races, invalidation, thundering herd); Martin Kleppmann, Designing Data-Intensive Applications (durability and write-behind trade-offs); Caffeine/Guava CacheLoader documentation for read-through loaders. Re-authored and deepened for this guide to correct the orthogonality error and add cache-aside, write-back, write-around, eviction/TTL behavior, and the durability trade-off.

🤖 Don't fully get this? Learn it with Claude

Stuck on ReadThrough vs WriteThrough Cache? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **ReadThrough vs WriteThrough Cache** (System Design) and want to truly understand it. Explain ReadThrough vs WriteThrough Cache 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.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **ReadThrough vs WriteThrough Cache** 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.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **ReadThrough vs WriteThrough Cache** 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.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **ReadThrough vs WriteThrough Cache** 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.

📝 My notes