Skip to main content
Pattern

Cache-aside (Lazy loading)

Problem

Reads dominate your workload, and every read hits a slow store (database, disk, network). Latency and load on the primary storage climb until the primary becomes the bottleneck.

Context

You have a read-heavy workload (say 100:1 read:write), a working set that fits comfortably in memory, and read-through freshness of a few seconds is acceptable. This is the most common cache pattern in the industry.

Solution

The application manages the cache directly. On a read, the app checks the cache first; on a hit, it returns immediately. On a miss, the app reads from the primary store, writes the result into the cache with a TTL, and returns. On a write, the app writes to the primary store and either invalidates the cache entry or writes it through. The cache is not on the write path by default — hence 'aside'.

Trade-offs

  • Every cache miss pays a double round-trip (cache read + primary read + cache write)
  • Cache and primary can diverge briefly on a race (write invalidates, concurrent read repopulates stale)
  • TTL choice is a trade-off: short = fresher but more misses; long = better hit rate but staler
  • Application code must handle every access — no free consistency guarantee

Failure modes

  • Cache stampede — TTL expires on a hot key, N concurrent readers all miss, all fetch from primary simultaneously. Mitigate with request coalescing, jittered TTL, or refresh-ahead.
  • Stale-forever bug — invalidation logic misses an edge case; stale entries live indefinitely. Mitigate with belt-and-suspenders TTL.
  • Cache poisoning — malformed cached value serves incorrect data. Mitigate by validating cache reads.
  • Cache warmer stampede on cold start — after a Redis restart, every key misses at once. Mitigate with gradual warm-up + primary rate limits.

When to use

  • Read-heavy workloads with tolerable staleness (feeds, catalogs, sessions)
  • You want maximum control over what's cached and when
  • The cache can hold your entire working set with TTL rotation

When NOT to use

  • Write-heavy workloads — cache maintenance dominates, and hit rate is low
  • Strong consistency required on every read — use write-through or don't cache
  • Small dataset that fits in the primary's own page cache — you're just adding a hop
DEEP DIVE
Naive → Failure → Fix

Why the problem occurs

The physical laws of storage are inescapable. RAM access: ~100ns. SSD: ~100μs. Network to a database: ~1ms. Cross-region DB: ~50-100ms. When your product needs sub-100ms response times but data lives in a database 50ms away, math forces the same answer everyone reaches: keep hot data close to the CPU. The moment your read:write ratio exceeds ~10:1 (common: feeds, product catalogs, user profiles, permission checks), the arithmetic of caching wins by an order of magnitude on both latency and cost. This is the same insight that gave us CPU L1/L2/L3 caches, page cache, browser cache — Cache-aside is that pattern at the application tier.

Naive solution (what most engineers try first)

Naive attempt: `def get(key): return db.get(key)`. Every read hits the DB. At 100 rps, DB is fine. At 10,000 rps, DB is on fire. First reaction from a junior engineer: 'add a cache!' They write: `def get(key): if key in cache: return cache[key]; else: v = db.get(key); cache[key] = v; return v`. Looks right. Ships to production. Two weeks later, the payments team is livid because their users are seeing outdated balances that never refresh. No TTL. No invalidation. That's the failure mode of the naive cache implementation.

Why the naive solution fails

The naive implementation has three fatal bugs: 1. **No expiration.** A cached value lives forever, even if the source of truth changed. Now cache is a lie. Users see stale data indefinitely. 2. **No invalidation on write.** When the app writes new value to DB, the cache still holds the old value. Read-after-write breaks: user updates their profile, immediately refreshes, sees the old profile. 3. **No stampede protection.** When one cache entry expires or is evicted, and 1000 requests hit that key simultaneously, all 1000 miss the cache and hit the DB at once. The DB, which was fine at 10K QPS with the cache in front, now sees a 1000-QPS spike concentrated on a single query. It falls over. The cache went from 'reducing DB load' to 'amplifying DB load during expiry.' A production-grade cache-aside implementation has to solve all three: TTL for eventual consistency, explicit invalidation for read-after-write, and stampede protection for hot keys.

Mental model

Think of cache-aside as **the application being the librarian for a hot-book collection**. The librarian (your app) has a small desk (Redis/Memcached) where the most-requested books sit. When you ask for a book: (1) librarian checks the desk — if there, hand it to you. (2) If not, librarian walks to the stacks (DB), gets the book, and PLACES A COPY ON THE DESK before handing you the original. Next time someone asks, it's on the desk. When someone updates a book: they update the master copy in the stacks (DB write), then either throw away the desk copy (invalidate) or update it (write-through). The 'aside' name comes from: writes go through the librarian to the stacks — the desk is 'aside' from the write path.

Step-by-step flow

  1. 1. Client calls `service.getUser(userId)`
  2. 2. Service checks Redis: `GET user:${userId}` — cache hit? Return immediately (~1ms round trip)
  3. 3. Cache miss — Service calls `db.query('SELECT * FROM users WHERE id = ?', userId)` (~5-50ms)
  4. 4. Service serializes result to JSON, writes to Redis with TTL: `SETEX user:${userId} 300 ${json}` (5 min TTL)
  5. 5. Service returns the value to client
  6. 6. On next read within 5 min: cache hit, ~1ms — savings of 4-49ms per request AND removed load from the DB
  7. 7. **Write path** — Client calls `service.updateUser(userId, patch)`
  8. 8. Service writes to DB: `UPDATE users SET ... WHERE id = ?`
  9. 9. Service invalidates cache: `DEL user:${userId}` (next read repopulates from DB)
  10. 10. **Alternative write path (dual-write, more risk)** — write to DB, then update cache with new value. Risk: DB succeeds, cache write fails. Now cache holds a stale value with no TTL knowledge that it's stale.

Implementation concepts

  • **TTL (Time-To-Live)** — every cached entry has an expiration. Set based on staleness tolerance: 60s for user preferences, 5min for product catalog, 30s for feed rankings.
  • **TTL jitter** — never set the same TTL on every key. Randomize ±20% to avoid mass expiration. `ttl_seconds = base + random(0, base * 0.4)`.
  • **Stampede protection (single-flight)** — when key expires, only ONE request should fetch from DB; others wait for that fetch to complete. Implement via distributed lock (Redis SETNX) or promise-de-duplication in-process.
  • **Refresh-ahead** — proactively refresh keys nearing expiration before they miss. Background job or async refresh on read-through.
  • **Negative caching** — cache misses (key not found) also cost the DB round trip. Cache the 'not found' sentinel for 30-60s to prevent DB pounding on invalid keys.
  • **Cache versioning** — include a version prefix in cache keys: `v3:user:${id}`. Bumping the version instantly invalidates all keys — the fastest 'flush' in production.
  • **Serialization format** — JSON is universal but expensive. MessagePack, protobuf, or MsgPack for hot paths cuts serialization cost 5-10x.
Understand

Mental model

Think of cache-aside as **the application being the librarian for a hot-book collection**. The librarian (your app) has a small desk (Redis/Memcached) where the most-requested books sit. When you ask for a book: (1) librarian checks the desk — if there, hand it to you. (2) If not, librarian walks to the stacks (DB), gets the book, and PLACES A COPY ON THE DESK before handing you the original. Next time someone asks, it's on the desk. When someone updates a book: they update the master copy in the stacks (DB write), then either throw away the desk copy (invalidate) or update it (write-through). The 'aside' name comes from: writes go through the librarian to the stacks — the desk is 'aside' from the write path.

Architect's decision

Alternatives compared

Read-through cache (cache-through)
Pros

Application code is simpler — cache library handles miss and DB fetch. No cache-management logic in your app.

Cons

Requires cache library that speaks your DB protocol. Fewer options. Tighter coupling to a specific cache technology. Less control over cache logic.

Verdict

Great for orgs with a mature caching library. If your org doesn't already have one, cache-aside is simpler operationally.

Write-through cache
Pros

Cache is always in sync — no stale reads possible. Great for high-consistency needs.

Cons

Every write pays double latency (write to cache + write to DB). Not tolerant of cache being down (write fails).

Verdict

Use when read-your-writes is strict and write throughput is modest. Cache-aside wins for write-heavy or when cache downtime is acceptable.

Write-back (write-behind) cache
Pros

Writes go to cache first, DB updated asynchronously — fastest writes possible.

Cons

Data loss risk if cache dies before DB write happens. Requires durable cache (Redis with AOF), which erodes the speed advantage.

Verdict

Niche — session updates, view counters. Never for durable data.

In-process cache (Caffeine, Guava)
Pros

Zero network hop, sub-microsecond access, no external dependency. Great for extremely hot, small data.

Cons

Per-instance — no cross-instance consistency. Wasted memory if you have 100 app servers each caching the same data.

Verdict

Use for tiny, hot data (feature flags, config, geo-lookup tables). Use Redis for anything that needs to be shared across instances.

CDN (edge cache)
Pros

Zero backend hit for cached content. Latency <20ms globally.

Cons

Only works for cacheable HTTP responses (GET, no auth, no user-specific data). Invalidation slower and per-CDN.

Verdict

Layer ON TOP of app cache. CDN for public content; Redis for authenticated/dynamic data.

Materialized views in DB
Pros

Cache lives IN the DB, synchronously updated by triggers or async by CDC. No separate cache tier to operate.

Cons

Only shifts work to a different DB workload. Doesn't reduce DB load.

Verdict

Useful for expensive aggregations. Not a substitute for cache-aside for hot key lookups.

Interview

Interview ladder

beginner
  • What is cache-aside and why do we use it?
  • Trace a cache-hit vs cache-miss request.
  • What is TTL and how do you pick a value?
  • Compare cache-aside with write-through in one paragraph.
senior
  • How do you handle a cache stampede after a hot-key expires?
  • Walk through your invalidation logic on a UPDATE user query.
  • Debug: 'user updates profile, refreshes page, sees old data.'
  • How do you size a Redis cluster for 100M users?
  • Compare cache-aside vs read-through vs write-through — when does each win?
staff
  • Design a multi-region cache-aside system with tolerable staleness.
  • How do you handle cache warming after a full Redis cluster restart?
  • Your Redis hit rate dropped from 95% to 60% — what do you check first?
  • Design a cache-aside layer over 100 sharded MySQL primaries.
  • How would you migrate from cache-aside to a different pattern without downtime?
principal
  • We're moving to a multi-cloud architecture. What's your cache-aside strategy?
  • How does cache-aside interact with our data privacy story? (GDPR, right to delete)
  • Should we build a company-wide caching platform, or let each team manage their own?
  • What's the 5-year evolution of our caching architecture as we scale from 10M to 1B users?

Real-world examples

Twitter
Source ↗

Home timeline is precomputed and cache-aside stored in Redis. Every tweet post fan-outs writes to followers' cached timelines. Reads are pure cache hits from Redis for the vast majority of requests.

Facebook
Source ↗

TAO (The Associations and Objects) is Facebook's read-optimized cache-aside layer over MySQL. Every social graph read hits TAO first; TAO fetches from MySQL on miss and repopulates. Handles trillions of reads/day at <1% miss rate.

Instagram
Source ↗

User profile reads are cache-aside via Cassandra + memcached. Their engineering blog details their cache warming strategy after node failures.

Every subreddit page is cache-aside from Postgres via memcached. Front page (r/all) hot content served entirely from cache during traffic spikes (Elon-tweets-about-something-and-your-site-melts problem).

Netflix
Source ↗

EVCache (their fork of memcached) handles cache-aside for user preferences, movie metadata, and playback state. Multi-region replication for global cache freshness.

Interview questions

  • Walk me through cache-aside read and write paths for a user-profile endpoint.
  • How do you handle the thundering herd on a hot key that just expired?
  • Explain the trade-off between short and long TTLs. How do you pick?
  • You get a bug report: 'I updated my profile and it's still showing the old data.' Debug.
  • How do you invalidate cached data after a DB write?
  • What's the difference between cache-aside and read-through?
  • When would you choose write-through over cache-aside?
  • How does the cache tier fail? What's your fallback?
  • How would you cache data that must be strongly consistent?
  • Design cache-aside for a multi-region system.

Practice exercise

Implement cache-aside for a user-profile service. Include: (1) cache-hit path, (2) cache-miss path with DB fallback, (3) TTL with jitter, (4) invalidation on updateUser(), (5) stampede protection via distributed lock. Test with 1000 concurrent requests to a single expired key.

Knowledge graph

Learn these first

  • Basic caching concepts (hit, miss, eviction)
  • TTL — Time To Live semantics
  • Redis or Memcached at a fundamental level
  • Distributed systems: understanding eventual consistency
  • Application-DB request/response flow

Where this appears in the curriculum