CMD Guide
HomeSystem DesignCaching

Cache Read Strategies

A read strategy decides who does the work on a cache miss — the application (cache-aside) or the cache library itself (read-through) — and that single choice determines where your data-fetch logic lives, how a cache outage propagates, and which staleness races you are exposed to. Both strategies produce the same steady-state behaviour on a hit (return from cache, skip the database); they differ only on the miss path and in who owns it.

The mechanism, side by side

On a hit, both are identical: the caller asks the cache, the cache returns the value, the database is never touched. The interesting path is the miss.

The consequence: with read-through, a single bug in the loader is fixed in one place; with cache-aside, every call site that reads that key must repeat the miss-then-load-then-populate dance (or funnel through a shared helper).

diagram
diagram

A worked trace with real numbers

Assume a Redis lookup takes 1 ms, a Postgres row fetch takes 40 ms, TTL is 300 s, and we serve user:42. Watch the same three requests under cache-aside.

tRequestCacheWhat the app doesLatency
0 msGET user:42missRedis GET (1) → miss → Postgres (40) → Redis SET (1)~42 ms
50 msGET user:42hitRedis GET (1) → return~1 ms
120 msGET user:42hitRedis GET (1) → return~1 ms

The first request pays the full 42 ms penalty (this is why a cold cache is slower than no cache — an extra 1 ms Redis round-trip on top of the DB read). The next two ride at 1 ms, a 40× speedup. Under read-through the numbers are identical; the only difference is that steps 2–4 happen inside the cache client, so your application code just sees cache.get("user:42") return in 42 ms then 1 ms.

Now the payoff of caching: if 95 of every 100 requests hit, average latency is 0.95×1 + 0.05×42 = ~3 ms instead of a flat 40 ms — and, more importantly, 95% of the read load never reaches Postgres.

Cache-aside in code (and why the naive version is wrong)

The obvious implementation has a correctness bug on the write path — it updates the DB and then the cache non-atomically, which loses the point of caching under concurrency.

// NAIVE — has a stale-write race
func GetUser(id string) (User, error) {
    if v, ok := cache.Get("user:" + id); ok {
        return v.(User), nil
    }
    u, err := db.LoadUser(id)      // cache miss → app loads
    if err != nil { return User{}, err }
    cache.Set("user:"+id, u, 300)  // populate
    return u, nil
}

// Elsewhere, a writer:
func UpdateUser(u User) error {
    db.SaveUser(u)                 // 1) write DB
    cache.Set("user:"+u.ID, u, 300)// 2) write cache  ← RACE
}

Why it is wrong: interleave a reader and this writer. Reader misses and loads the old row (v1) at step A. Writer commits v2 to the DB and does cache.Set(v2) at step B. Then the slow reader finally runs its cache.Set(v1) — overwriting v2 with the stale v1, which now lives until the TTL expires (up to 300 s of serving stale data). The fix is to invalidate, not update, the cache on writes, and let the next read repopulate:

func UpdateUser(u User) error {
    if err := db.SaveUser(u); err != nil { return err }
    cache.Delete("user:" + u.ID)   // invalidate; next read reloads v2
    return nil
}

This does not fully eliminate the race (a read that loads v1 and sets it after the delete can still resurrect a stale value), but it shrinks the window dramatically and is the standard cache-aside write policy. Fully closing it needs versioned keys or a short lock — see the stampede/invalidation lesson.

Pitfalls

When to use it / when NOT to

Signals that point to cache-aside: read-heavy, tolerant of eventual consistency, and you want the cache to be optional infrastructure — if it fails, the app still works by hitting the DB. You also want per-key control (different TTLs, selective caching, conditional population). This is why Redis + application code is the default at most companies.

Signals that point to read-through: many call sites read the same entities and you want the load logic centralized in one place (DRY, testable, uniform); you are using a caching library/provider that supports a loader (e.g. Caffeine's LoadingCache in Java, Ehcache, a Guava CacheLoader, or a managed read-through tier). You accept that the cache is now on the critical path.

Trade-offs vs the named alternative

Choose cache-aside when resilience and fine-grained control matter more than code tidiness — the common web-backend case. Prefer read-through when the same entity is read from many places and you want one authoritative, well-tested load path, and you can guarantee the cache tier's availability.

Takeaways


Sources: Martin Fowler, Cache-Aside and enterprise caching patterns; Microsoft Azure Architecture Center, Cache-Aside pattern; AWS caching best-practices (ElastiCache); the Caffeine and Ehcache documentation for read-through/LoadingCache semantics; and standard treatments in Alex Xu, System Design Interview. Worked latencies are representative Redis/Postgres figures. Re-authored and deepened for this guide.

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

Stuck on Cache Read Strategies? 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 **Cache Read Strategies** (System Design) and want to truly understand it. Explain Cache Read Strategies 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 **Cache Read Strategies** 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 **Cache Read Strategies** 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 **Cache Read Strategies** 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