Caching that actually helps
Pick the right caching pattern and the right layer, and know what a cache costs you.
Open this lesson in the learning hubKey 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.