Filter concepts by levelShowing all levels.

System Design · Caching Fundamentals

Caching Fundamentals

Why caching works, the four patterns, and how entries leave a cache

  • Localitytemporal + spatial access skew
  • Four patternsaside, read-through, write-through, write-back
  • TTL / eviction / invalidationthree ways an entry leaves
  • Warming + negative cachingcold-start and miss protection

1.Why caching works: locality and avoiding repeated work

Caching works because real access patterns are not random — the same data tends to be requested again soon (temporal locality), and related data tends to be requested together (spatial locality). A cache stores the result of expensive work once and serves it repeatedly from somewhere far cheaper to read, exploiting exactly those patterns.

Key features

  • Temporal locality: data accessed recently is likely to be accessed again soon (a trending article, a logged-in user's own profile)
  • Spatial locality: data near recently accessed data is likely to be accessed next (adjacent rows, related records fetched together)
  • Caching only helps when the underlying computation or fetch is genuinely expensive relative to a cache read — caching something already fast adds cost for no benefit
  • A cache trades memory (or a faster storage tier) for avoiding repeated CPU, I/O, or network cost
  • Caching helps most when access patterns are skewed — a small number of keys account for a large share of requests (the "hot" subset)

What makes data a good caching candidate

What makes data a good caching candidate
PropertyGood fit for cachingPoor fit for caching
Read/write ratioRead far more often than writtenWritten as often as read
Access skewA small hot subset gets most requestsAccess is close to uniformly random
Cost to produceExpensive query, computation, or external callAlready cheap to read directly
Staleness toleranceSlightly stale data is acceptableMust always reflect the very latest write
Database load, with and without a cache

Without a cache

  • Every request runs the expensive 4-table join (~40ms)
  • 10,000 requests/min → ~6.7 minutes of cumulative DB time
  • Load scales linearly with traffic — no ceiling

With a cache (95% hit rate)

  • 9,500 requests/min served from cache in ~1ms each
  • Only 500 requests/min still reach the database
  • Database load drops by ~95% for the same traffic
  • Without a cache
    • Every request runs the expensive 4-table join (~40ms)
    • 10,000 requests/min → ~6.7 minutes of cumulative DB time
    • Load scales linearly with traffic — no ceiling
  • With a cache (95% hit rate)
    • 9,500 requests/min served from cache in ~1ms each
    • Only 500 requests/min still reach the database
    • Database load drops by ~95% for the same traffic

Signature

caching pays off when: expensive to produce + read >> written + skewed access

Caching works because real access is skewed, not random — the same or related data gets requested again soon. It only pays off when the underlying work is genuinely expensive and read far more often than it's written.

2.Cache-aside, read-through, write-through and write-back

These four patterns answer two questions: who is responsible for loading data into the cache, and when does a write update the cache relative to the underlying store. Cache-aside puts the application in charge of both; read-through and write-through hand that responsibility to the cache itself; write-back further delays writing to the underlying store until later, trading durability risk for write speed.

Key features

  • Cache-aside (lazy loading): application checks the cache first; on a miss, reads from the store and populates the cache itself — the most common pattern
  • Read-through: the cache sits in front of the store and loads on a miss automatically — the application only ever talks to the cache
  • Write-through: every write goes to the cache and the store together, synchronously — cache is always consistent with the store, at the cost of write latency
  • Write-back (write-behind): a write updates the cache immediately and is written to the store asynchronously later — fastest writes, but a crash before the async write means data loss
  • Cache-aside and write-through are the two most common combinations in practice; write-back is reserved for workloads that can tolerate its durability risk

The four cache patterns

The four cache patterns
PatternWho loads on a missWrite pathRisk
Cache-asideApplicationApp writes to store; cache is invalidated or updated separatelyMomentary inconsistency between store and cache
Read-throughCache itselfSame as cache-aside, or paired with write-throughCache library/proxy needs store-loading logic
Write-throughCache or applicationSynchronous: cache + store both updated before write returnsHigher write latency
Write-backCache or applicationAsynchronous: cache updated now, store updated laterData loss if the cache fails before flushing
Cache-aside read path, on a miss
1. check2. read3. fill

Application

checks cache first

Cache miss

not found

Database

source of truth

Cache

app writes the result in

  • Application — checks cache first
    • leads to Cache miss (1. check)
  • Cache miss — not found
    • leads to Database (2. read)
  • Database — source of truth
    • leads to Cache (3. fill)
  • Cache — app writes the result in

Command

text
cache-aside:  app -> cache (miss) -> store -> app fills cache
read-through: app -> cache -> (miss) store, cache fills itself
write-through: app -> cache + store (both, synchronously)
write-back:    app -> cache (immediately) -> store (later, async)

Cache-aside: app manages both the miss and the write path — most common. Read-through: the cache loads itself. Write-through: cache and store updated together, synchronously. Write-back: cache updated now, store later — fastest, riskiest.

3.TTL, eviction and invalidation

These are the three ways an entry leaves a cache. A TTL (time-to-live) expires an entry automatically after a set duration. Eviction removes entries when the cache is full, following a policy like least-recently-used (LRU). Invalidation is an explicit, immediate removal triggered by the application when it knows the underlying data changed — the only one of the three that reacts to an actual write rather than time or space pressure.

Key features

  • TTL: an entry expires automatically after a fixed duration, regardless of whether the underlying data changed
  • Eviction: when the cache is full, a policy (commonly LRU — least recently used) decides which entry to remove to make room for a new one
  • Invalidation: the application explicitly removes or updates an entry the moment it knows the underlying data changed — the most precise, but requires the application to track every place a value is cached
  • TTL and eviction can both leave a cache serving stale data for a bounded window; invalidation, done correctly, does not — but is the hardest to get right consistently
  • Cache invalidation being hard is a famous, well-earned reputation — every "delete this key" call has to be found, and missed paths silently return stale data

The three ways an entry leaves a cache

The three ways an entry leaves a cache
MechanismTriggerGuarantees freshness?
TTLTime elapsesNo — stale for up to the TTL window
Eviction (e.g. LRU)Cache is full, needs spaceNo — evicted for space, not staleness
InvalidationApplication detects a writeYes, if every write path correctly invalidates
Three ways an entry leaves the cache

TTL

expires after a fixed duration

Eviction

removed under space pressure (LRU)

Invalidation

explicit removal on a known write

  1. TTL — expires after a fixed duration
  2. Eviction — removed under space pressure (LRU)
  3. Invalidation — explicit removal on a known write

Command

text
SET user:42 "..." EX 300   -- TTL: expires in 300s
-- eviction: cache-internal, policy-driven (e.g. LRU)
DEL user:42                -- invalidation: explicit

TTL expires by time, eviction removes by space pressure, invalidation removes by an explicit write signal — invalidation is the most precise but the easiest to miss a path for, which is why a TTL backstop is usually kept even alongside it.

4.Cache warming and negative caching

Cache warming pre-loads likely-needed entries before real traffic arrives, avoiding a wave of cache misses right after a cold start or deploy. Negative caching stores the fact that something does NOT exist (a 404, a not-found lookup) so repeated requests for the same missing item don't hit the expensive backend every time — an easily overlooked case, since it feels odd to "cache" an absence.

Key features

  • Cache warming: proactively populating a cache with expected-hot entries before it starts serving real traffic
  • Without warming, a freshly started or newly deployed cache is empty — every early request is a miss, which can spike backend load right when a deploy is already a risky moment
  • Negative caching: storing "this does not exist" for a lookup that failed, so repeated identical misses don't repeatedly hit the backend
  • Negative-cache entries usually need a short TTL of their own — the underlying item might be created shortly after being cached as "missing"
  • Without negative caching, a client repeatedly requesting a nonexistent ID (a bug, or abuse) forces a full backend lookup every single time

Cache warming vs negative caching

Cache warming vs negative caching
TechniqueProblem it solvesTypical trigger
Cache warmingCold-start miss storm right after deploy/restartPre-load top-N known-hot keys on startup
Negative cachingRepeated lookups for something that doesn't existCache the miss result itself, with a short TTL
Two holes in "just cache what exists"

One hole opens before the cache has anything in it. The other never closes, because the key it is asked for will never resolve.

  • Two panels. The left panel is cache warming, the right is negative caching.
  • Left: at 09:00 the cache is redeployed empty, 90 percent of the first 30 seconds are misses, and every miss lands on the database. Warming pre-loads the top-N hot keys on startup so the first requests hit a warm cache.
  • Right: a request for /users/999999999, an id that does not exist, is repeated. The cache stores hits only, so every ask reaches the database. The fix is to cache the miss itself with a short TTL, after which repeats hit the cache.

Command

text
-- warming: populate before serving traffic
for key in top_1000_products: cache.set(key, load(key))

-- negative caching: cache the miss itself
if not found_in_db(id):
    cache.set(f"user:{id}", NOT_FOUND, ttl=60)

Warming pre-loads a cache before real traffic hits it, avoiding a cold-start miss storm. Negative caching stores "not found" results too, so repeated misses for the same missing key don't bypass the cache entirely.

Remember

  • Caching pays off when data is expensive to produce, read far more than written, and access is skewed toward a hot subset.
  • Cache-aside is the most common pattern; write-back trades durability for write speed.
  • A TTL bounds staleness even when invalidation logic misses a write path — keep both.
  • A cache with no negative-caching path gives zero protection against repeated requests for missing data.

Pro tip

  • Delete (don't overwrite) a cache entry on write in cache-aside — it avoids a race leaving a stale value in place.
  • Warm a cache before a deploy finishes rolling out, not after traffic has already resumed.

Next up

  • Redis and distributed caching
  • CDN and edge caching

Sources

  • Redis documentation — redis.io/docs
  • System Design Primer — github.com/donnemartin/system-design-primer
  • Checked 2026-08-21

Spotted a mistake on this page? Tell us.