Caching & CDN· 20 min

Caching

Redis cache-aside, write-through, write-behind patterns

CachingFundamental20 min read

Store frequently accessed data in fast memory to reduce latency and database load.

Caching — diagram
🖱️ Drag nodes · Scroll to zoom · Drag background to pan
1. GET2. check3a. HIT3b. MISS4. data5. SET6. responseClientApp ServerRedis CacheTTL: 5min · LRUDatabasePostgreSQL

High-level design

Client → App Server → [Cache (Redis) ← miss → DB]. On read: check cache first; if hit, return; if miss, query DB, populate cache, return. On write: update DB, then either invalidate or update cache.

Low-level design

Cache-aside (lazy load): app manages cache explicitly. Read-through: cache fetches from DB on miss. Write-through: write goes to cache + DB synchronously. Write-behind: write to cache, async flush to DB. Eviction: LRU, LFU, TTL. Cache stampede prevention: mutex/lock on miss.

Trade-offs

  • →Cache-aside is simple but app logic is complex; read-through is transparent but couples cache to DB
  • →Write-through is consistent but slow; write-behind is fast but risks data loss
  • →TTL is simple but causes thundering herd; LRU adapts but needs memory tracking

Used in production by

Redis, Memcached, Varnish (HTTP), CDN edge caches

Questions to ask your interviewer

  • →What's the cache hit rate target?
  • →How stale can data be?
  • →What's the cache size vs working set?

System design deep dive

Caching: deep dive

Caching keeps hot data in memory so most reads never touch the database, cutting latency from milliseconds to microseconds and shrinking database load by an order of magnitude. Getting it right is mostly about invalidation, stampedes, and knowing what you can afford to serve stale.

How a cache-aside read and write work, step by step

  1. A request for product 42 arrives at the app server, which builds a cache key such as product:42:v3 (the version suffix lets you invalidate an entire schema change at once).
  2. The app issues GET product:42:v3 to Redis. A hit returns in roughly 0.2 to 0.5 ms over the network, and the request is done.
  3. On a miss, the app queries the database (typically 2 to 20 ms), serializes the row, and writes it back with SET product:42:v3 <json> EX 600 so it expires after 10 minutes.
  4. When the product is updated, the app first commits the change to the database and then deletes the cache key rather than overwriting it.
  5. The next read misses, reloads fresh data, and repopulates the cache. Deleting instead of setting avoids a race where two concurrent writers leave an older value in the cache.

The arithmetic is what sells caching in an interview. With a 95% hit rate, only 1 in 20 reads reaches the database, so a database that handles 5,000 reads per second can now back roughly 100,000 reads per second at the application tier.

Where caches live

  • Client and browser: HTTP Cache-Control headers, free but hard to invalidate.
  • Edge: a CDN for static assets and cacheable API responses.
  • In-process: a local map or Caffeine/Guava cache inside each server. Fastest possible, but every instance has its own copy, so invalidation is per instance.
  • Distributed: Redis or Memcached shared by all app servers. One source of truth for cached values, at the cost of a network hop.
  • Database internal: the buffer pool and query caches the database already maintains.

When to use it (and when not to)

Cache data that is read far more often than it is written and that tolerates a little staleness: user profiles, product catalogs, rendered pages, session data, computed leaderboards. Be cautious with data where a stale read causes real harm, such as account balances or inventory counts at checkout; read those from the source of truth. Also skip caching when the access pattern has no locality, for example a long tail where almost every key is requested once, because the hit rate will be too low to justify the memory.

Classic cache failure modes

ProblemWhat happensMitigation
Cache stampedeA hot key expires and thousands of requests hit the DB at onceSingle-flight lock per key, or refresh early before expiry
Cache penetrationRequests for keys that do not exist always miss and hit the DBCache negative results briefly, or front with a Bloom filter
Cache avalancheMany keys share the same TTL and expire togetherAdd random jitter to TTLs (for example 600s plus up to 60s)
Hot keyOne key gets so much traffic it overloads a single cache nodeReplicate the key, add a local in-process layer for it
Stale after writeA race leaves old data cached after an updateDelete on write, short TTL as a safety net, versioned keys
Cache-aside with a stampede guard
Product getProduct(long id) {
    String key = "product:" + id;
    Product cached = redis.get(key);
    if (cached != null) return cached;

    // Only one caller per key rebuilds; others wait briefly and retry.
    if (redis.setIfAbsent("lock:" + key, "1", Duration.ofSeconds(5))) {
        try {
            Product fresh = db.findProduct(id);
            int ttl = 600 + random.nextInt(60);        // jitter avoids avalanches
            redis.set(key, fresh, Duration.ofSeconds(ttl));
            return fresh;
        } finally {
            redis.delete("lock:" + key);
        }
    }
    sleep(50);
    return getProduct(id);                           // retry, usually a hit now
}

Common mistakes in interviews

  • Adding a cache box without saying what is cached, the key format, the TTL, or the expected hit rate.
  • Never discussing invalidation, which is the hard part interviewers want to hear about.
  • Using write-behind for data that cannot be lost, without acknowledging the durability risk if the cache node crashes.
  • Assuming the cache always holds everything. Size it against the working set: caching the top 20% of 10 million objects at 2 KB each needs about 4 GB.
  • Ignoring what happens when the cache cluster is down. The database must survive a cold-cache restart, or you need gradual warm-up.

Interview questions

›Why delete the cache entry on update instead of writing the new value?

Two concurrent updates can finish their database writes in one order and their cache writes in the other, leaving the older value cached indefinitely. Deleting forces the next reader to load from the database, which is always current. A short TTL bounds the damage if a delete is ever lost.

›How do you choose between LRU and LFU eviction?

LRU evicts what has not been touched recently and adapts quickly to shifting popularity, which suits most web workloads. LFU keeps items that are consistently popular even if they were not used in the last few seconds, which protects against a one-off scan flushing the cache. Redis supports both via maxmemory-policy.

›How would you keep in-process caches consistent across 50 servers?

Keep TTLs short, and broadcast invalidation events through pub/sub (Redis pub/sub or a message queue) so each server evicts its local copy. For data that must be consistent everywhere, skip the local layer and use only the shared distributed cache.

›What is the difference between Redis and Memcached for caching?

Memcached is a simple multi-threaded key-value store with very predictable performance. Redis adds rich data structures (sorted sets, hashes, streams), persistence, replication, and Lua scripting, which makes it useful for leaderboards, rate limiters, and queues as well as plain caching.

›How do you warm a cache after a cold start?

Preload the most popular keys from access logs or a precomputed list before taking traffic, and shift traffic onto the new cluster gradually. Request coalescing ensures that the misses that remain do not all reach the database at once.

More System Design Concepts

Compare this lesson with the other 13 concepts in the course. Expand any card for its design, trade-offs and diagram.

Category:
Difficulty:
Showing 13 of 13