DEV Community

Cover image for Caching Is Easy. Cache Invalidation Is the Hard Part.
Ritesh Gupta
Ritesh Gupta

Posted on

Caching Is Easy. Cache Invalidation Is the Hard Part.

Imagine you're building an e-commerce application.

A user opens a product page. Instead of querying the database every time, you store the product details in Redis. The page loads faster, and your database has less work to do.

Everything looks great. Then someone updates the product price from ₹999 to ₹1,199. The database has the correct price. But your application keeps showing ₹999.

Nothing is broken in the database. Your API is responding. Redis is working exactly as expected.

Yet the user is seeing the wrong information.

This is the tricky part of caching. Making data faster to access is relatively straightforward. Keeping that cached data accurate is where things get interesting.

Let's understand why cache invalidation is such an important problem in system design.

1. Why Do We Need Caching in the First Place?

Consider a simple application where every request goes directly to the database.

User Request
     |
     v
 Application Server
     |
     v
   Database
     |
     v
    Response
Enter fullscreen mode Exit fullscreen mode

This approach works perfectly well for small applications. But as traffic increases, the database may receive thousands of repeated queries for the same information.

For example, imagine a popular product page receives 10,000 requests in a short period. If every request queries the database for the same product details, we're doing a lot of repetitive work.

Instead, we can store frequently accessed data in a cache.

User Request
     |
     v
 Application Server
     |
     v
    Cache
     |
     |-- Cache HIT --> Return cached data
     |
     |-- Cache MISS --> Query database
                           |
                           v
                      Store in cache
                           |
                           v
                       Return data
Enter fullscreen mode Exit fullscreen mode

A cache is a fast storage layer that keeps copies of data so the application can retrieve them without repeatedly accessing the original data source.

Redis is a popular choice, although caches can also exist inside an application, in a browser, or at a CDN edge.

The benefits are straightforward:

  • Lower response latency
  • Fewer database queries
  • Reduced load on backend services
  • Better performance during periods of high traffic

But there's one important detail: a cache stores a copy of the data, not the original source of truth.

That distinction explains the problem we're about to explore.

2. What Exactly Is Cache Invalidation?

Cache invalidation means making cached data unusable when it is no longer valid, so that the application can fetch or generate a correct replacement.

Let's continue with our e-commerce example.

Initially, the database contains:

{
  "product_id": 101,
  "name": "Wireless Headphones",
  "price": 999
}
Enter fullscreen mode Exit fullscreen mode

The application fetches this record and caches it.

A few minutes later, an administrator updates the price:

{
  "product_id": 101,
  "name": "Wireless Headphones",
  "price": 1199
}
Enter fullscreen mode Exit fullscreen mode

The database now contains ₹1,199, but the cache still contains ₹999.

If the application returns the cached record, the customer sees an outdated price.

This is called stale data.

The challenge is deciding when cached data should be removed, refreshed, or treated as invalid.

And the difficulty increases when multiple application servers, caches, and services are involved.

3. Three Common Approaches to Cache Invalidation

There isn't one universal solution. The right approach depends on how frequently the data changes and how much staleness the application can tolerate.

Approach 1: TTL-Based Expiration

TTL stands for Time to Live.

It defines how long an entry can remain in the cache before it expires.

For example, we might cache product details for five minutes.

cache.set(
    "product:101",
    product_data,
    ex=300
)
Enter fullscreen mode Exit fullscreen mode

Here, ex=300 represents a 300-second expiration when using the Redis Python client.

After the TTL expires, Redis treats the entry as expired. The next cache lookup will need to retrieve the data again if no valid entry exists.

Advantages:

  • Simple to implement
  • Automatically limits how long old entries remain cached
  • Useful for data that doesn't require immediate updates

Disadvantages:

  • Data may remain stale until the TTL expires
  • Frequently accessed entries may still need to be recomputed after expiration
  • Short TTLs increase cache misses and database load

One important point: TTL does not guarantee that data stays fresh for the entire time window. It only limits how long an entry remains valid according to its expiration.

For example, if a product price changes one second after the cache is populated, users could still see the old price for almost five minutes.

TTL is useful when a little staleness is acceptable. It isn't sufficient by itself when updates must become visible quickly.

Approach 2: Cache-Aside Pattern

Cache-aside is one of the most common caching patterns in backend applications.

The application checks the cache first. If the data isn't there, it reads from the database and stores the result in the cache.

Here's a simplified Python example using a Redis client and a database access function:

import json

def get_product(product_id, redis, db):
    cache_key = f"product:{product_id}"

    cached = redis.get(cache_key)

    if cached is not None:
        return json.loads(cached)

    product = db.get_product(product_id)

    if product is None:
        return None

    redis.set(
        cache_key,
        json.dumps(product),
        ex=300
    )

    return product
Enter fullscreen mode Exit fullscreen mode

The flow is:

  1. Check Redis for the product.
  2. If the entry exists, return it.
  3. Otherwise, query the database.
  4. Store the result in Redis with a TTL.
  5. Return the result to the caller.

This approach keeps the application in control of when data is loaded into the cache.

But notice something important: cache-aside describes how data is read and populated. It doesn't automatically solve invalidation after an update.

We need to handle writes separately.

Approach 3: Explicit Invalidation After a Write

Suppose an administrator updates a product price.

A common strategy is to update the database first and then delete the corresponding cache entry.

def update_product_price(
    product_id,
    new_price,
    db,
    redis
):
    db.update_price(product_id, new_price)

    redis.delete(f"product:{product_id}")
Enter fullscreen mode Exit fullscreen mode

The next request won't find the old cache entry. It can read the updated product from the database and populate the cache again.

This is a simple and effective strategy for many applications.

However, there's a subtle issue.

What happens if the database update succeeds but the cache deletion fails?

The application could continue serving stale data until the cache entry expires or another mechanism removes it.

Distributed systems also introduce race conditions. For example, a request that started reading the old database value before the update might write that old value into the cache after the invalidation occurs.

So even explicit deletion isn't a complete guarantee of consistency under every possible failure or concurrency scenario.

For systems where stale data is especially costly, engineers may need retries, reliable event processing, versioning, or other coordination mechanisms.

4. Cache Invalidation vs. Cache Refresh

These terms are related, but they aren't identical.

Invalidation makes an existing cache entry unusable. The next request may fetch fresh data from the source of truth.

Refresh updates the cached value with newer data, either when a request arrives or in the background.

For example, after a product price changes:

  • Invalidation: Delete product:101 from Redis.
  • Refresh: Fetch the updated product and overwrite product:101.

Invalidation is often simpler because the application doesn't have to immediately rebuild every cached value.

Refreshing can be useful when an entry is expensive to regenerate and likely to be requested again soon.

The trade-off is that refresh logic introduces additional work and failure cases. A refresh can fail, or it can overwrite newer data with an older result if concurrency isn't handled carefully.

5. What Happens When Multiple Servers Are Involved?

So far, we've considered one application and one cache.

Now imagine your application has ten server instances, all reading and writing product data.

If each server maintains its own in-memory cache, invalidating data on one server won't automatically invalidate it on the others.

                 Load Balancer
                       |
          +------------+------------+
          |            |            |
          v            v            v
       Server A     Server B     Server C
          |            |            |
       Cache A      Cache B      Cache C
          |            |            |
          +------------+------------+
                       |
                    Database
Enter fullscreen mode Exit fullscreen mode

This is one reason shared caching systems such as Redis are useful: multiple application instances can access the same cache.

But a shared cache doesn't eliminate every consistency problem.

There can still be concurrent requests, delayed invalidation, failed operations, replica lag, and stale values already held by other layers, such as application memory or a CDN.

For larger systems, engineers may use mechanisms such as:

  • Event-driven invalidation: Publish an event when data changes and have relevant services invalidate their caches.
  • Versioned cache keys: Include a version in the key so old entries are no longer selected after an update.
  • TTL as a fallback: Ensure that entries eventually expire even if an invalidation event is missed.
  • Reliable event delivery: Use durable messaging or an outbox pattern when losing invalidation events would be unacceptable.

These approaches have their own trade-offs. Event-driven invalidation, for example, can reduce stale-data windows, but it doesn't automatically provide immediate consistency because events may be delayed or retried.

The important lesson is that cache invalidation becomes a coordination problem as a system grows.

6. A Related Problem: Cache Stampede

There's another issue worth understanding.

Imagine a popular cached item expires just as thousands of users request it.

Many requests miss the cache at approximately the same time. They all query the database and try to repopulate the same entry.

This sudden burst of work is commonly called a cache stampede.

Instead of reducing database load, the cache expiration creates a spike in database traffic.

Some ways to mitigate this include:

  • Request coalescing: Let one request load the missing value while other requests wait for its result.
  • TTL jitter: Add a small random variation to expiration times to avoid many entries expiring simultaneously.
  • Background refresh: Refresh frequently accessed data before it expires.
  • Stale-while-revalidate: Serve an older value temporarily while refreshing it in the background, when the application can safely tolerate that staleness.

These techniques solve a different problem from invalidation itself, but they matter when designing a reliable caching strategy.

7. How Do You Choose the Right Strategy?

There is no need to use the most complicated solution everywhere.

Start by asking a few questions:

How frequently does the data change?

For mostly static content, a longer TTL may be enough. For frequently updated records, explicit invalidation may be more appropriate.

How much staleness is acceptable?

A slightly outdated recommendation might be acceptable. An outdated account balance or incorrect price at checkout can have much more serious consequences.

What happens if invalidation fails?

If stale data can cause significant harm, think beyond a simple cache deletion. Consider retries, durable events, versioning, and a clear consistency guarantee.

How expensive is recomputing the data?

If regenerating a value is costly, refreshing it before expiration or coalescing concurrent misses may help.

The goal isn't to eliminate every possible stale read at any cost. It's to choose a strategy whose performance, complexity, and consistency guarantees fit the requirements.

8. The Biggest Lesson I Took From This

Before exploring caching in depth, it's easy to think of it as a simple optimization: put frequently accessed data into Redis and let the application read it faster.

But caching introduces another copy of the data, and now the system has to manage the relationship between that copy and the source of truth.

That changes the problem.

You're no longer thinking only about how quickly a request can return a response. You're also thinking about what happens when data changes, requests overlap, network calls fail, and multiple services disagree temporarily.

A fast system isn't necessarily a correct system. And a good system design has to account for both.

Caching is powerful, but it works best when you understand what you're trading away, how stale data is handled, and what guarantees your application actually needs.

The next time you add a cache to a project, don't stop at asking, "How can I make this faster?"

Also ask:

"When this data changes, how will the rest of my system find out?"

That question can make a much bigger difference than adding another layer of caching.


Want to Practice System Design Beyond Theory?

Understanding concepts like caching, cache invalidation, and distributed systems is a great start. But the real learning happens when you try to apply them to actual system design problems.

That's one reason I've been exploring ScaleDojo — a platform for learning and practicing system design through interactive challenges.

Instead of only reading about architectures, you can work through design problems, think about trade-offs, and improve your understanding of how real-world systems are put together.

If you're learning system design for backend engineering interviews or want to get better at designing scalable applications, it's worth checking out.

Explore ScaleDojo: https://scaledojo.dev/

My takeaway: don't just memorize how distributed systems work. Practice making the decisions that make them work.

Top comments (0)