Back-end · Mid-level

Backend caching: when to use it and how to invalidate

Caching is one of the most powerful tools for making a system fast — and one of the easiest to get wrong. The famous line "the two hardest things in computer science are naming things and cache invalidation" exists for a reason. Knowing when to cache, how to invalidate and when not to cache is expected of a mid-level dev.

Why cache

Caching means storing the result of an expensive operation so you can reuse it later instead of redoing it. The operation might be a heavy query, a call to another API or a slow computation. The analogy: instead of going to the store every time you want milk, you keep some in the fridge. The cost is that the milk in the fridge can go bad — and that's where all the complexity lives.

The cache-aside pattern

The most common pattern is cache-aside (also called lazy loading): the application looks at the cache first; if the data is there (hit), it uses it; if not (miss), it fetches from the source, stores it in the cache and returns it.

function getProduct(id):
    value = cache.get("product:" + id)
    if value != null:
        return value              // cache hit
    value = db.query(id)          // cache miss
    cache.set("product:" + id, value, ttl=300)
    return value

The upside: only what is actually requested gets cached. The downside: the first access to each item is always slow (the initial miss).

TTL and expiration

The TTL (Time To Live) defines how long a value stays in the cache before it expires automatically. It's the simplest way to avoid data staying stale forever. A short TTL keeps data fresher but reduces hit rate; a long TTL gets more reuse but risks serving old data. Choosing the TTL is balancing freshness against performance.

Write strategies

Cache-aside deals with reads. When data changes, there are write strategies. The most direct is write-through: when writing, you update the database and the cache together, keeping them in sync. Other options include invalidating the key on write (letting the next read reload it). What matters is having an answer to "what happens to the cache when the data changes?".

Why invalidation is hard

Cached data is a copy, and copies go stale when the source changes. You have to decide when to discard them — and getting it wrong in either direction hurts: invalidating too much throws away the benefit of the cache; invalidating too little serves wrong data. Worse: invalidating the right key, at the right moment, everywhere it exists, is genuinely hard in distributed systems.

Caching is easy; the hard part is knowing exactly when to throw away the stored data. Every "why is it showing the old value?" bug is an invalidation problem.

Where the cache lives

LocationSpeedShared?
Process memoryExtremely highNo (only that server)
Redis / MemcachedVery highYes (across servers)
CDNHigh (close to the user)Yes (public/static content)

Local in-memory cache is the fastest but doesn't scale across instances — which is why Redis is so widely used: a shared cache that all instances query. This connects with scalability, since stateless services usually delegate state to an external cache.

When NOT to cache

Caching isn't free. Avoid it when: the data changes on every access (the cache never hits), it requires absolute accuracy (bank balance, critical inventory), or it's rarely read (not worth the complexity). Day to day, caching too early is a common source of subtle bugs — measure first, cache later.

Conclusion

Caching trades freshness for speed. Master the cache-aside pattern, set deliberate TTLs, have an invalidation strategy and learn to recognize the cases where caching brings more problems than benefits. It's a huge performance lever — as long as you respect the hard part.

Read also

Back to track