A cache trades freshness for speed. Every caching decision is really a decision about how stale a piece of data may be, and answering that question first makes the implementation straightforward.
Layers, from cheapest to most complex
- Browser caching for static assets with long lifetimes and versioned filenames
- CDN caching for public pages and media
- Application caching for expensive computed results
- Query caching for repeated database reads
- Object caching for hot records
Invalidation is the hard part
Prefer short lifetimes over clever invalidation where you can tolerate it. Where you cannot, invalidate on write from a single place in the code so every path benefits. Scattering invalidation across controllers guarantees a missed one.
Cache keys deserve thought
Include everything that changes the output: identifiers, locale, permissions, pagination. A key that omits a variable serves one user's data to another, which is a security incident rather than a bug.
Watch for stampedes
When a popular key expires, every request may recompute it simultaneously. Use a lock or a probabilistic early refresh so one request does the work while others serve the slightly stale value.
Measure the hit rate
A cache with a poor hit rate adds latency and complexity for nothing. If you are not measuring it, you do not know whether it helps.
Decide how stale each piece of data may be before you write a line of caching code. That single answer determines everything else.
Where to start
Find your slowest endpoint, check whether its result depends on anything that changes per request, and cache it if not. Measure the hit rate after a week.


