System Design
Fundamentals
Cache Invalidation Strategies

Cache Invalidation Strategies

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

Caching is the most effective way to improve application performance and reduce database load. However, because cached data is a copy of the source of truth (the database), a critical problem arises: how do we ensure the cache is updated or cleared when the source data changes?

This is the challenge of Cache Invalidation. If handled incorrectly, your application will serve stale, outdated, or corrupted data to users.


1. Caching Read/Write Patterns

How your application reads and writes data determines the relationship between your cache and your database. There are four primary patterns.

A. Cache-Aside (Lazy Loading)

This is the most common caching pattern. The application is responsible for coordinating both the database and the cache.

  • Read Flow: App checks the cache. On a Cache Hit, it returns the data. On a Cache Miss, it reads from the DB, writes the data to the cache, and returns it.
  • Write Flow: App writes the updated data directly to the database, and then deletes (invalidates) the corresponding key from the cache.
  • Pros: Resilient to cache failure (if cache dies, requests fallback to DB). Cache only contains data that is actually requested (memory efficient).
  • Cons: Cache miss latency penalty on the first request.

B. Write-Through

The cache acts as the primary data store interface. The application writes directly to the cache, which synchronously updates the database.

  • Flow: The application writes data to the cache. The cache immediately writes it to the database. Only after both writes succeed does the application receive a confirmation.
  • Pros: Data is never stale; cache and database are always in lockstep. Subsequent reads are extremely fast.
  • Cons: Higher write latency because every write requires two network hops (Cache + DB) synchronously.

C. Write-Behind (Write-Back)

Similar to Write-Through, but the database write happens asynchronously in the background.

  • Flow: The application writes data to the cache. The cache immediately returns success to the client. The cache then queues the write and updates the database asynchronously (in batches).
  • Pros: Extremely fast write performance. Ideal for write-heavy workloads (e.g., real-time game state tracking, IoT telemetry).
  • Cons: High risk of data loss. If the cache server crashes before flushing the queue to the database, the data is lost permanently.

D. Refresh-Ahead

The cache dynamically predicts access patterns and reloads popular keys before they expire.

  • Flow: If a key has a TTL of 10 minutes and is heavily requested at minute 9, the cache automatically fetches the latest value from the database in the background to prevent a cache miss.
  • Pros: Reduces read latency to near-zero for hot keys.
  • Cons: Difficult to configure accurately; can waste database bandwidth if predicted keys are never read.

Caching Strategy Trade-offs

StrategyRead PerformanceWrite PerformanceConsistencyData Loss Risk
Cache-AsideHigh (after first miss)High (direct write + delete)Eventual (until deleted)None
Write-ThroughMaximumLow (synchronous double write)StrongNone
Write-BehindMaximumMaximumEventualHigh (cached queues can crash)
Refresh-AheadMaximumN/A (depends on read pattern)EventualNone

2. Active Invalidation Mechanisms

Once data changes in the database, you must choose how to purge the stale cache.

A. TTL (Time-to-Live)

Every cached key is assigned an expiration time (e.g., SET key value EX 300 for 5 minutes).

  • Best For: Semi-static data (e.g. user profiles, pricing lists) where eventual consistency is acceptable.
  • Pro: Automatic cleanup; prevents Redis from filling up with dead keys.
  • Con: Data remains stale for the duration of the TTL.

B. Explicit Purging (Active Delete)

When a write event occurs (e.g., UPDATE users SET name = 'Bob' WHERE id = 123), the application code explicitly triggers a delete query on the cache (DEL user:123).

  • Pro: Immediate consistency.
  • Con: Increases code complexity; if the delete command fails due to a network blip, the cache remains stale forever until a TTL clears it.

C. Stale-While-Revalidate

A hybrid approach where the cache serves the stale data immediately on a read request, but asynchronously fires a background query to fetch the updated data from the database and refresh the cache.

  • Pro: Zero read latency for users while maintaining near-real-time updates.
  • Con: Users will see old data exactly once before the update.

3. Common Caching Disasters & How to Mitigate Them

A. Cache Stampede (Thundering Herd)

  • The Scenario: A highly popular key (e.g., home page config) expires. At that exact millisecond, 50,000 concurrent users request it. Because the key is missing, all 50,000 requests hit the database simultaneously, causing a database crash.
  • Mitigation:
    • Mutex Locking: Use a distributed lock so only the first request gets permission to fetch from the database while the other 49,999 wait.
    • XFetch (Probabilistic Early Expiration): Refresh the key background-wise slightly before its actual expiration time.

B. Cache Penetration

  • The Scenario: An attacker requests non-existent keys (e.g., GET /user/9999999) repeatedly. Since these IDs do not exist, the cache never stores them, and every single query hits the database.
  • Mitigation:
    • Cache Nulls: Store a temporary null value in the cache with a short TTL (e.g., SET user:9999999 NULL EX 60).
    • Bloom Filters: Use a space-efficient probabilistic data structure (like a Bloom Filter) at the cache layer to quickly determine if an ID is completely invalid before querying the DB.

C. Cache Avalanche

  • The Scenario: Your team deploys a database migration and writes all data into the cache with a uniform TTL of 6 hours. Exactly 6 hours later, the entire cache expires at the same second. The database is instantly crushed by the massive traffic spike.
  • Mitigation:
    • Randomized TTL (Jitter): Add a small random offset to the TTL when caching keys (e.g., instead of exactly 6 hours, set it to 6 hours + random_minutes(0, 30)).