Why Cache Invalidation Is Harder Than Caching

"There are only two hard things in Computer Science: cache invalidation and naming things."

– Phil Karlton – and every engineer who has debugged stale cache at 2 AM

The Deceptive Simplicity of Caching

Adding a cache to a backend system is one of the most satisfying performance improvements an engineer can make. The change is often small – point your application at Redis, add a few lines of get/set logic, and watch your database query times collapse from hundreds of milliseconds to single digits. The dashboard turns green. The latency percentiles improve dramatically. Everyone is happy.

And then, several weeks later, a user reports that their profile still shows the old email address. Or a customer calls support because the product price they were quoted has changed. Or an engineer notices that the homepage featured products haven’t updated since Tuesday, even though the database was updated hours ago.

The cache was the hero of the performance story. Now it’s the villain of the correctness story.

Putting data into a cache is easy. Knowing when that cached data is no longer correct – that is the genuinely hard problem.

Caching solves a performance problem. Cache invalidation solves a correctness problem. Performance is desirable. Correctness is non-negotiable.

01

What Is Cache Invalidation?

Cache invalidation means determining when cached data should no longer be considered valid – and then removing or updating it before any consumer can act on the stale version. The word ‘invalidation’ is precise: it means rendering the cached entry no longer authoritative, regardless of whether the entry itself has been deleted or simply marked expired.

Before the update: database and cache agree. After: the database has the truth; the cache has history.

The diagram above captures the core problem. After Alice changes her name to Alicia, the database reflects the new reality. The cache does not. The system now has two sources of truth – and they disagree. No error is raised. No alarm fires. The application keeps running. But any user whose request is served from cache sees a name that no longer exists.

This is what makes cache staleness particularly insidious: the system appears healthy by every conventional metric. The database is up. Redis is responding. The application is processing requests. Error rates are normal. Yet some fraction of users are receiving incorrect data, and you may not find out until they tell you.

02

Why 'Just Delete the Cache' Isn't Enough

The first instinct is correct in principle: whenever the database changes, delete the corresponding cache entry. The next read will miss the cache, fetch fresh data from the database, and repopulate the cache with the correct value. Clean. Simple.

The problem is that in distributed systems, ‘update database then delete cache’ is two separate operations against two separate systems. No transaction spans both. Either one can fail independently, and they can execute in unexpected orders relative to concurrent requests.

Failure Mode 1 - The Cache Delete Fails

// What you intend:
db.update('user:101', { name: 'Alicia' });  // ✓ succeeds
redis.del('user:101');                   	// ✗ network blip - fails silently
 
// Result: DB has Alicia. Cache has Alice. Indefinitely.
// No exception raised. No alert. Users see wrong data.

Failure Mode 1 - The Cache Delete Fails

// What happens if you delete the cache first:
redis.del('user:101');      	// ✓ cache cleared
db.update('user:101', {...});   // ✗ DB timeout - update fails
 
// Result: cache is empty, DB has old data.
// Next read repopulates cache WITH THE OLD VALUE.
// Data appears consistent, but update was silently lost.

The order of operations matters enormously. ‘Delete cache then update DB’ and ‘update DB then delete cache’ produce different failure modes.
Neither order is fully safe without additional safeguards. Both can result in stale cache in specific failure scenarios.

03

The Race Condition That Defeats Correct Invalidation

Here is the most subtle and frustrating failure mode. The engineer writes perfectly correct invalidation code. The cache is deleted after every database write. And yet – under specific timing conditions – the cache ends up stale.

Same operations. Different timing. The ‘safe’ sequence works; the ‘dangerous’ sequence produces permanent staleness.

The dangerous sequence happens when a reader’s cache miss occurs in the narrow window between the writer’s cache delete and the writer’s database update. The reader fetches from the database, gets the old value (because the write hasn’t happened yet), and writes that old value back into the freshly invalidated cache. The writer then updates the database – but the cache, which was just cleared, now contains the pre-update value again.

This is not a theoretical edge case. Under any meaningful write load, this window will be hit regularly. The stale value can persist until the next write to that key or until the TTL expires – potentially a very long time.

The race condition shows that invalidation alone isn’t sufficient. The sequence of cache-delete and DB-write must be managed carefully, and concurrent reads during the write window need to be considered.
Longer-lived caches and higher write frequencies both increase the probability of hitting this window.

04

Caching Strategies and Their Consistency Properties

The race condition and failure modes above arise primarily with the most common pattern: cache-aside. Understanding the full range of caching strategies – and their different consistency trade-offs – is essential for choosing the right tool for each scenario.

Cache-aside gives you control but demands correctness. Write-through gives you consistency but costs write latency.

Cache-Aside (Lazy Loading)

The most common pattern. The application manages the cache explicitly. On read: check cache first, fall back to database on miss, populate cache with the result. On write: update the database, then invalidate the cache entry.

Advantages: the cache only contains data that has actually been requested, making it memory-efficient. The application has full control over what is cached and when. 

Disadvantages: the application must handle invalidation correctly at every write path – and miss any path, and you have a stale cache. Susceptible to the race condition described above.

Write-Through

The application writes to the cache, and the cache synchronously writes to the database. Reads always hit the cache (no misses after the first write). The cache and database are kept in sync on every write.

Advantages: strong consistency between cache and database – no staleness on read paths. 

Disadvantages: every write pays the latency of both cache and database. The cache grows to contain all written data, even rarely-read entries. Adds complexity in failure scenarios where the cache write succeeds but the DB write fails.

Write-Behind (Write-Back)

The application writes to the cache only. The cache asynchronously flushes to the database in the background. Excellent write performance; the application receives confirmation as soon as the cache write succeeds.

The trade-off is significant: if the cache is lost before the flush completes, data is permanently lost. Write-behind is appropriate for high-frequency writes of non-critical data – telemetry, counters, analytics – but should never be used where durability of writes is required.

05

TTL - The Escape Hatch With Hidden Costs

Time To Live (TTL) is the most widely used tool in the cache invalidation toolkit – and the most widely misunderstood. The promise is appealing: set an expiry time on every cached entry, and staleness becomes bounded. After ten minutes, the entry is gone and fresh data will be fetched.

TTL trades freshness for simplicity. The right TTL depends entirely on the business tolerance for stale data.

But TTL does not solve cache invalidation – it reframes it. Instead of asking ‘when should I invalidate this?’, TTL makes you ask ‘how stale am I willing to serve?’ These are different questions with very different answers depending on the data.

Short TTL (seconds–minutes) Long TTL (hours–days)
  • ✓ Data refreshes frequently
  • ✓ Changes become visible quickly
  • X More cache misses under load
  • X More database queries
  • X Lower effective cache hit ratio

Use for: prices, stock levels, session data

  • ✓ High cache hit ratio
  • ✓ Low database pressure
  • ✓ Excellent read performance
  • X Stale data persists longer
  • X User-visible correctness issues

Use for: static content, user preferences, config

The practical guidance: use TTL as a safety net against infinite staleness, not as your primary invalidation strategy. Pair it with explicit invalidation for data that changes frequently and where freshness matters.

06

Event-Driven Invalidation and the Outbox Pattern

For systems with multiple write paths – admin APIs, mobile APIs, background jobs, webhooks – explicit invalidation in the application code creates a dangerous maintenance burden. Every path that can modify the data must also correctly invalidate the cache. Miss one, and you have a stale cache that no TTL will clean up promptly.

The more scalable approach is to decouple the cache invalidation from the write path entirely, using events. When a product is updated, publish a ProductUpdated event. A dedicated cache consumer subscribes to these events and invalidates the relevant cache keys. Now the write paths don’t need to know anything about caching.

But this approach introduces a new problem: the dual-write problem. Updating the database and publishing an event to a message broker are two separate operations against two separate systems. If the event publish fails after the database update succeeds, the cache consumer never receives the notification and the cache remains stale indefinitely – with no error raised.

The Transactional Outbox Pattern: database write and event creation are one atomic transaction. Delivery is guaranteed by the outbox publisher.

The Transactional Outbox Pattern

The outbox pattern solves the dual-write problem by storing the event in the same database transaction as the data update. The event is not published directly to the message broker – it is written to an ‘outbox’ table in the same commit.

-- Both operations succeed or both fail - atomically
BEGIN TRANSACTION;
 
UPDATE products SET price = 799 WHERE id = 123;
 
INSERT INTO outbox_events (event_type, payload, created_at)
VALUES ('ProductUpdated', '{"productId": 123}', NOW());
 
COMMIT;
 
-- A separate Outbox Publisher process polls the outbox table,
-- publishes events to the message broker, and marks them delivered.
-- If publishing fails, it retries - guaranteed at-least-once delivery.

The outbox publisher can retry failed deliveries as many times as needed. The cache consumer, upon receiving the event, deletes the relevant cache keys. Because the event and the data update are in the same transaction, the cache will always eventually be invalidated – even if the broker was temporarily unavailable at the moment of the update.

The outbox pattern shifts your reliability guarantee from ‘at-most-once event delivery’ (direct publish) to ‘at-least-once delivery’ (outbox + retry).
At-least-once delivery means your cache consumer must be idempotent – processing the same invalidation event twice should be safe. Deleting a cache key that doesn’t exist is always safe, so cache invalidation consumers are naturally idempotent.

07

The Cache Key Dependency Problem

As a system grows, the same underlying data is often represented in multiple cached forms. A product exists not only as product:123 but also as part of category listings, search results, homepage widgets, and promotional banners. When the product changes, all of these representations become stale simultaneously.

Product 123 changes. Six cache keys need invalidation. Missing any one of them leaves users seeing stale data.

The diagram shows a realistic scenario: one product update potentially invalidates six cache keys. This is not unusual in e-commerce systems. The challenge is not deleting the keys – that part is easy. The challenge is knowing which keys to delete, and ensuring that list stays accurate as the system evolves.

Practical Approaches to Key Dependency Management

Building agents that need to remember things?

Join the CodeKerdos Spring Boot + AI Bootcamp. Weekends, hands-on, built for working Java developers who want to ship real agentic systems, not just toy demos.

codekerdos.in | Follow along with Week 11 next weekend

Scroll to Top