Caching that actually helps

System Design · lesson 4 of 32 · 4 min read

Pick the right caching pattern and the right layer, and know what a cache costs you.

Open this lesson in the learning hub

Key points

  • A cache keeps the answer near the asker. A Redis hit is around a millisecond; the query it replaces is often a hundred times that.
  • Cache-aside is the default: read cache, on a miss read the database, then store the result. The app stays alive if the cache dies.
  • Write-through writes cache and database together: fresher, slower. Write-behind queues the database write: fast, can lose data.
  • Bound every cache with a max size and a TTL. An unbounded cache is a memory leak with better marketing.
  • Local (Caffeine) is fastest but per-instance, so instances disagree. Redis is shared and survives restarts. Big systems use both.
  • Only cache what is hot and expensive. Caching a cheap query just moves the cost and adds a staleness bug.

Example

// Cache-aside with Caffeine (com.github.benmanes.caffeine:caffeine)
Cache<String, Product> cache = Caffeine.newBuilder()
        .maximumSize(10_000)                       // bound the memory
        .expireAfterWrite(Duration.ofMinutes(5))   // bound the staleness
        .recordStats()                             // hit ratio is the only proof it works
        .build();

// One loader call per key, even if 200 threads miss at the same instant.
Product p = cache.get(id, key -> repo.findById(key).orElseThrow());

// Spring does the same thing declaratively:
@Cacheable(cacheNames = "products", key = "#id")
public Product find(String id) { return repo.findById(id).orElseThrow(); }

Cache-aside with a size limit and a TTL solves 90% of read load. Start there.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the System Design course, and every lesson in it is listed on the System Design contents page.