Introduction

Cache invalidation is one of the hardest problems in software engineering. Serving stale data frustrates users, but invalidating too aggressively defeats the purpose of caching. Here are proven strategies that balance freshness with performance.

Key Concepts

  • Active Invalidation: Explicitly deleting cache entries when source data changes.
  • Version-Based Invalidation: Including a version number in cache keys so old entries are naturally orphaned.
  • Event-Driven Invalidation: Using Pub/Sub or message queues to notify cache consumers of data changes.

Real World Context

When a user updates their profile, every page showing their name must reflect the change immediately. Without a clear invalidation strategy, some pages show the old name for minutes or hours — a common source of user complaints.

Deep Dive

Strategy 1: Time-Based Invalidation (TTL)

redis
# Let caches expire naturally
SET cache:data "..." EX 300

# Pros: Simple, automatic
# Cons: Data may be stale until expiration

Strategy 2: Active Invalidation

Delete cache when source data changes:

redis
# On database UPDATE/INSERT/DELETE:
DEL cache:user:1001
DEL cache:user:1001:profile
DEL cache:api:users:list

Pattern: Namespace Invalidation

When updating data affects multiple cache keys:

redis
# Product update affects multiple caches
# Option A: Delete all related keys
DEL cache:product:123
DEL cache:products:category:electronics
DEL cache:products:featured
DEL cache:search:electronics

# Option B: Use SCAN to find and delete
SCAN 0 MATCH cache:product:123:* COUNT 100
# Delete each returned key

Pattern: Event-Driven Invalidation

Publish invalidation events:

redis
# When data changes, publish event
PUBLISH cache:invalidation '{"type":"product","id":123}'

# Subscribers delete their caches
# SUB receives message → DEL cache:product:123

Strategy 3: Version-Based Invalidation

Include version in cache key:

redis
# Store version
SET product:123:version 5

# Cache with version in key
SET cache:product:123:v5 "{...}" EX 3600

# On update:
INCR product:123:version  # Now v6
# Old cache (v5) is orphaned, will expire
# New requests use v6 key

Pros: Clean invalidation, no explicit delete needed Cons: Orphaned keys waste memory until TTL

Strategy 4: Generation/Epoch-Based

For invalidating all caches at once:

redis
# Store cache generation
SET cache:generation 42

# Include generation in all cache keys
SET cache:g42:user:1001 "..." EX 3600

# To invalidate ALL caches:
INCR cache:generation  # Now 43
# All old g42 caches are orphaned

Handling Cache Stampede

When cache is invalidated, multiple requests may try to rebuild it:

Solution 1: Locking

redis
# First request acquires lock
SET cache:user:1001:lock "1" NX EX 10
# If acquired:
#   - Query database
#   - Update cache
#   - Delete lock
# If not acquired:
#   - Wait briefly
#   - Retry GET from cache

Solution 2: Stale-While-Revalidate

redis
# Store with soft and hard TTL
SET cache:data '{"value":"...","soft_ttl":1704067200}' EX 400

# On read:
# If past soft_ttl:
#   - Return stale data immediately
#   - Trigger async refresh

Invalidation Patterns Summary

PatternComplexityConsistencyUse Case
TTL OnlyLowEventualTolerance for staleness
Active DELMediumStrongWrite-heavy apps
Version-BasedMediumStrongClean invalidation needed
Event-DrivenHighStrongDistributed systems

Common Pitfalls

  1. Only using TTL-based invalidation — For data that changes unpredictably, TTL alone means users see stale data until expiration. Combine with active invalidation for consistency.
  2. Forgetting related cache keys — Updating a product affects not just cache:product:123 but also cache:products:featured, search results, and category listings. Map all dependencies.

Best Practices

  1. Start with TTL + active DEL — This simple combination covers most use cases: TTL provides a safety net while DEL handles known mutations.
  2. Use version-based invalidation for complex dependency graphs — When a change affects many cache keys, incrementing a version is cleaner than tracking and deleting each one.

Summary

  • TTL-based invalidation is simplest but allows temporary staleness
  • Active DEL provides strong consistency when you know which keys to invalidate
  • Version-based invalidation avoids explicit deletes by orphaning old keys
  • Event-driven invalidation scales best for distributed systems

📖 Caching Best Practices

Code Examples

bash
# Active invalidation: delete cache when data changes
DEL cache:user:1001
DEL cache:user:1001:profile

# Version-based invalidation: increment version
INCR product:123:version
# Old key cache:product:123:v5 is orphaned
# New requests use cache:product:123:v6

# Event-driven invalidation via Pub/Sub
PUBLISH cache:invalidation '{"type":"product","id":123}'
✓ Completed