home / skills / amnadtaowsoam / cerebraskills / cache-invalidation

cache-invalidation skill

/04-database/cache-invalidation

This skill helps you implement robust cache invalidation strategies across TTL, event-driven, write-through, and tag-based patterns to improve data freshness.

npx playbooks add skill amnadtaowsoam/cerebraskills --skill cache-invalidation

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
33.3 KB
---
name: Cache Invalidation Strategies
description: Patterns and strategies for cache invalidation - one of the two hardest problems in computer science.
---

# Cache Invalidation Strategies

## Overview

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

Cache invalidation is the process of removing or updating cached data when the underlying data changes. Done incorrectly, it leads to stale data being served to users. Done correctly, it ensures data consistency while maintaining performance benefits.

## Prerequisites

- Understanding of caching concepts and benefits
- Knowledge of Redis or similar caching technologies
- Familiarity with database operations and data consistency
- Basic understanding of distributed systems

## Key Concepts

### Why Cache Invalidation is Hard

Cache invalidation is challenging because:

1. **Multiple Cache Layers**: Data may be cached at multiple levels (browser, CDN, application cache, database cache)
2. **Complex Dependencies**: Cached data may depend on multiple data sources
3. **Timing Issues**: Changes may occur while cache is being updated
4. **Distributed Systems**: Multiple servers may have inconsistent cache states
5. **Performance Trade-offs**: Aggressive invalidation hurts performance, conservative invalidation risks stale data

### Cache Invalidation Patterns

#### Time-Based Expiration (TTL)

The simplest approach - cache entries expire after a fixed time.

**Pros:**
- Simple to implement
- No invalidation logic needed
- Works well for slowly changing data

**Cons:**
- May serve stale data until TTL expires
- Wastes cache space on unchanged data
- Hard to choose optimal TTL

#### Event-Driven Invalidation

Invalidate cache immediately when data changes.

**Pros:**
- Immediate consistency
- No stale data served
- Precise control

**Cons:**
- More complex implementation
- Requires event tracking
- May impact performance

#### Write-Through Cache

Write data to cache and database simultaneously.

**Pros:**
- Cache is always up-to-date
- Simple read logic

**Cons:**
- Writes are slower (cache + database)
- May write data that's never read

#### Write-Behind (Write-Back) Cache

Write to cache first, asynchronously persist to database.

**Pros:**
- Fast writes (cache only)
- Can batch database writes

**Cons:**
- Risk of data loss if cache fails
- Complex to implement
- Eventual consistency

#### Cache-Aside (Lazy Loading)

Check cache first, load from database on miss.

**Pros:**
- Simple to implement
- Only caches accessed data
- Good for read-heavy workloads

**Cons:**
- Cache stampede risk
- First request is slow
- Stale data until TTL expires

#### Read-Through Cache

Cache abstraction handles loading from database.

**Pros:**
- Clean separation of concerns
- Centralized cache logic
- Easy to add features

**Cons:**
- Slightly more complex
- Requires custom cache implementation

## Implementation Guide

### Time-Based Expiration (TTL)

```javascript
// Set cache with TTL
await cache.set('user:123', userData, { ttl: 3600 });  // 1 hour

// Get from cache
const cached = await cache.get('user:123');
if (cached) {
  return cached;
}

// Cache miss - fetch from database
const data = await db.users.findById(123);
await cache.set('user:123', data, { ttl: 3600 });
return data;
```

**Choosing TTL:**

```javascript
// Adaptive TTL based on data change frequency
function calculateTTL(dataType, lastModified) {
  const age = Date.now() - lastModified;

  switch (dataType) {
    case 'user_profile':
      return 3600;  // 1 hour - changes rarely
    case 'stock_price':
      return 5;     // 5 seconds - changes frequently
    case 'news_feed':
      return 300;    // 5 minutes - moderate changes
    default:
      return 600;    // 10 minutes default
  }
}

// Usage
const userData = await db.users.findById(123);
const ttl = calculateTTL('user_profile', userData.updatedAt);
await cache.set(`user:${userData.id}`, userData, { ttl });
```

### Event-Driven Invalidation

```javascript
// Update user and invalidate cache
async function updateUser(userId, updates) {
  // Update database
  const user = await db.users.update(userId, updates);

  // Invalidate cache
  await cache.del(`user:${userId}`);

  return user;
}

// Delete user and invalidate cache
async function deleteUser(userId) {
  await db.users.delete(userId);
  await cache.del(`user:${userId}`);
}
```

**Event Bus Pattern:**

```javascript
const EventEmitter = require('events');

class CacheInvalidator extends EventEmitter {
  constructor() {
    super();
    this.setupListeners();
  }

  setupListeners() {
    // Listen for data change events
    this.on('user:updated', async ({ userId }) => {
      await cache.del(`user:${userId}`);
      await cache.del(`user:${userId}:profile`);
      await cache.del(`user:${userId}:settings`);
    });

    this.on('post:created', async ({ userId }) => {
      // Invalidate user's posts cache
      await cache.del(`user:${userId}:posts`);
      // Invalidate feed cache
      await cache.del('feed:recent');
    });

    this.on('post:deleted', async ({ userId, postId }) => {
      await cache.del(`user:${userId}:posts`);
      await cache.del(`post:${postId}`);
    });
  }
}

// Usage
const invalidator = new CacheInvalidator();

async function createPost(userId, content) {
  const post = await db.posts.create({ userId, content });

  // Emit event
  invalidator.emit('post:created', { userId, postId: post.id });

  return post;
}
```

### Write-Through Cache

```javascript
async function updateUser(userId, updates) {
  const user = await db.users.update(userId, updates);

  // Write to cache
  await cache.set(`user:${userId}`, user, { ttl: 3600 });

  return user;
}

// Read always hits cache
async function getUser(userId) {
  const cached = await cache.get(`user:${userId}`);
  if (cached) {
    return cached;
  }

  const user = await db.users.findById(userId);
  await cache.set(`user:${userId}`, user, { ttl: 3600 });
  return user;
}
```

### Write-Behind (Write-Back) Cache

```javascript
class WriteBehindCache {
  constructor() {
    this.writeQueue = [];
    this.processing = false;
    this.startProcessing();
  }

  async set(key, value, options) {
    // Write to cache immediately
    await cache.set(key, value, options);

    // Queue for database write
    this.writeQueue.push({ key, value, timestamp: Date.now() });
  }

  startProcessing() {
    setInterval(async () => {
      if (this.writeQueue.length === 0 || this.processing) return;

      this.processing = true;

      try {
        const batch = this.writeQueue.splice(0, 100);  // Process in batches
        await this.writeBatchToDatabase(batch);
      } catch (error) {
        console.error('Write-behind error:', error);
        // Re-queue failed writes
        this.writeQueue.unshift(...batch);
      } finally {
        this.processing = false;
      }
    }, 100);  // Process every 100ms
  }

  async writeBatchToDatabase(batch) {
    // Batch write to database
    const operations = batch.map(({ key, value }) => {
      const [type, id] = key.split(':');
      return db[type].update(id, value);
    });

    await Promise.all(operations);
  }
}
```

### Cache-Aside (Lazy Loading)

```javascript
async function getUser(userId) {
  // Check cache
  const cached = await cache.get(`user:${userId}`);
  if (cached) {
    return cached;
  }

  // Cache miss - load from database
  const user = await db.users.findById(userId);

  // Populate cache
  await cache.set(`user:${userId}`, user, { ttl: 3600 });

  return user;
}
```

**Cache Stampede Prevention:**

```javascript
async function getUserWithStampedeProtection(userId) {
  const cacheKey = `user:${userId}`;
  const lockKey = `lock:${cacheKey}`;

  // Check cache
  const cached = await cache.get(cacheKey);
  if (cached) {
    return cached;
  }

  // Try to acquire lock
  const lock = await cache.set(lockKey, '1', {
    ttl: 10,  // Lock expires after 10s
    nx: true  // Only set if not exists
  });

  if (lock) {
    // We have the lock - load from database
    try {
      const user = await db.users.findById(userId);
      await cache.set(cacheKey, user, { ttl: 3600 });
      await cache.del(lockKey);
      return user;
    } catch (error) {
      await cache.del(lockKey);
      throw error;
    }
  } else {
    // Another request is loading - wait and retry
    await sleep(100);
    return getUserWithStampedeProtection(userId);
  }
}
```

### Read-Through Cache

```javascript
class ReadThroughCache {
  constructor(loader) {
    this.loader = loader;
  }

  async get(key) {
    // Check cache
    const cached = await cache.get(key);
    if (cached) {
      return cached;
    }

    // Cache miss - use loader
    const value = await this.loader(key);

    // Populate cache
    await cache.set(key, value, { ttl: 3600 });

    return value;
  }
}

// Usage
const userCache = new ReadThroughCache(async (key) => {
  const userId = key.split(':')[1];
  return await db.users.findById(userId);
});

const user = await userCache.get('user:123');
```

## Invalidation Strategies

### Purge (Delete Specific Key)

Remove a specific cache entry.

```javascript
// Simple purge
await cache.del('user:123');

// Multiple keys
await cache.del('user:123', 'user:123:profile', 'user:123:settings');

// Pattern-based purge (Redis)
await cache.del('user:123:*');
```

**When to use:**
- Single data source changed
- Simple key structure
- Precise invalidation needed

### Ban (Pattern-Based Invalidation)

Remove all cache entries matching a pattern.

```javascript
// Redis pattern-based deletion
async function banPattern(pattern) {
  const keys = await cache.keys(pattern);
  if (keys.length > 0) {
    await cache.del(...keys);
  }
}

// Usage
await banPattern('user:123:*');  // Delete all user 123's cache
await banPattern('feed:*');       // Delete all feed caches
```

**Tag-Based Invalidation:**

```javascript
class TaggedCache {
  constructor() {
    this.keyTags = new Map();  // key -> Set of tags
    this.tagKeys = new Map();  // tag -> Set of keys
  }

  async set(key, value, options = {}) {
    const tags = options.tags || [];

    // Store value
    await cache.set(key, value, options);

    // Update tag mappings
    this.keyTags.set(key, new Set(tags));

    for (const tag of tags) {
      if (!this.tagKeys.has(tag)) {
        this.tagKeys.set(tag, new Set());
      }
      this.tagKeys.get(tag).add(key);
    }
  }

  async invalidateByTag(tag) {
    const keys = this.tagKeys.get(tag);
    if (!keys) return;

    // Delete all keys with this tag
    const keyArray = Array.from(keys);
    await cache.del(...keyArray);

    // Clean up mappings
    for (const key of keyArray) {
      const tags = this.keyTags.get(key);
      tags.delete(tag);
      if (tags.size === 0) {
        this.keyTags.delete(key);
      }
    }

    this.tagKeys.delete(tag);
  }
}

// Usage
const taggedCache = new TaggedCache();

// Cache with tags
await taggedCache.set('user:123', userData, {
  tags: ['user', 'profile', 'premium']
});

await taggedCache.set('user:456', userData, {
  tags: ['user', 'profile']
});

// Invalidate all user caches
await taggedCache.invalidateByTag('user');

// Invalidate only premium user caches
await taggedCache.invalidateByTag('premium');
```

### Refresh (Update in Place)

Update cache with fresh data without removing it.

```javascript
async function refreshUser(userId) {
  // Fetch fresh data
  const user = await db.users.findById(userId);

  // Update cache in place
  await cache.set(`user:${userId}`, user, { ttl: 3600 });

  return user;
}

// Background refresh
async function backgroundRefresh(key, loader) {
  try {
    const freshData = await loader(key);
    await cache.set(key, freshData, { ttl: 3600 });
  } catch (error) {
    console.error('Background refresh failed:', error);
  }
}

// Proactive refresh before expiration
async function getWithProactiveRefresh(key, loader) {
  const cached = await cache.get(key);

  if (cached) {
    const ttl = await cache.ttl(key);

    // Refresh if TTL is below threshold (e.g., 10% remaining)
    if (ttl < 360) {  // 3600 * 0.1
      backgroundRefresh(key, loader);
    }

    return cached;
  }

  // Cache miss
  const data = await loader(key);
  await cache.set(key, data, { ttl: 3600 });
  return data;
}
```

### Soft Purge (Serve Stale While Revalidating)

Mark cache as stale but continue serving it while refreshing.

```javascript
class SoftPurgeCache {
  async get(key) {
    const cached = await cache.get(key);

    if (!cached) {
      return null;
    }

    // Check if marked for soft purge
    if (cached._stale) {
      // Trigger background refresh
      this.backgroundRefresh(key);

      // Return stale data
      return cached._data;
    }

    return cached;
  }

  async softPurge(key) {
    const cached = await cache.get(key);

    if (cached) {
      // Mark as stale but keep data
      await cache.set(key, {
        _stale: true,
        _data: cached,
        _purgedAt: Date.now(),
      });
    }
  }

  async backgroundRefresh(key) {
    const cached = await cache.get(key);

    // Check if already refreshing
    if (cached && cached._refreshing) {
      return;
    }

    // Mark as refreshing
    await cache.set(key, {
      ...cached,
      _refreshing: true,
    });

    try {
      const freshData = await this.loader(key);
      await cache.set(key, freshData, { ttl: 3600 });
    } catch (error) {
      console.error('Refresh failed:', error);
      // Remove refreshing flag
      await cache.set(key, { ...cached, _refreshing: false });
    }
  }
}
```

## Cache Stampede Prevention

### Lock-Based Prevention

```javascript
async function getWithLock(key, loader, ttl = 3600) {
  const cached = await cache.get(key);
  if (cached) {
    return cached;
  }

  const lockKey = `lock:${key}`;
  const lockValue = Date.now().toString();

  // Try to acquire lock
  const acquired = await cache.set(lockKey, lockValue, {
    ttl: 10,  // Lock expires after 10s
    nx: true,
  });

  if (acquired) {
    // We have the lock
    try {
      const value = await loader(key);
      await cache.set(key, value, { ttl });
      await cache.del(lockKey);
      return value;
    } catch (error) {
      await cache.del(lockKey);
      throw error;
    }
  } else {
    // Wait for lock holder
    await sleep(50);

    // Check cache again
    const cached = await cache.get(key);
    if (cached) {
      return cached;
    }

    // Still no cache, try again
    return getWithLock(key, loader, ttl);
  }
}
```

### Request Coalescing

```javascript
class RequestCoalescer {
  constructor() {
    this.pendingRequests = new Map();
  }

  async get(key, loader) {
    // Check if request is already pending
    if (this.pendingRequests.has(key)) {
      return await this.pendingRequests.get(key);
    }

    // Create new promise
    const promise = this.loadAndCache(key, loader);
    this.pendingRequests.set(key, promise);

    try {
      return await promise;
    } finally {
      this.pendingRequests.delete(key);
    }
  }

  async loadAndCache(key, loader) {
    const cached = await cache.get(key);
    if (cached) {
      return cached;
    }

    const value = await loader(key);
    await cache.set(key, value, { ttl: 3600 });
    return value;
  }
}

// Usage
const coalescer = new RequestCoalescer();

async function getUser(userId) {
  return await coalescer.get(`user:${userId}`, async (key) => {
    const id = key.split(':')[1];
    return await db.users.findById(id);
  });
}
```

## Distributed Cache Invalidation

### Pub/Sub Pattern

```javascript
const Redis = require('ioredis');

class DistributedCacheInvalidator {
  constructor() {
    this.publisher = new Redis();
    this.subscriber = new Redis();
    this.setupSubscriber();
  }

  setupSubscriber() {
    this.subscriber.psubscribe('cache:*');

    this.subscriber.on('pmessage', (pattern, channel, message) => {
      const [action, key] = channel.split(':').slice(1);

      switch (action) {
        case 'invalidate':
          cache.del(key);
          break;
        case 'invalidate_pattern':
          this.invalidatePattern(message);
          break;
      }
    });
  }

  async invalidate(key) {
    // Invalidate local cache
    await cache.del(key);

    // Notify other instances
    await this.publisher.publish(`cache:invalidate:${key}`, '');
  }

  async invalidatePattern(pattern) {
    const keys = await cache.keys(pattern);
    if (keys.length > 0) {
      await cache.del(...keys);
    }
  }

  async invalidatePatternDistributed(pattern) {
    // Invalidate local cache
    await this.invalidatePattern(pattern);

    // Notify other instances
    await this.publisher.publish(`cache:invalidate_pattern:${pattern}`, '');
  }
}
```

### Database Change Data Capture (CDC)

```javascript
const { DebeziumConnector } = require('debezium-connector');

class CDCInvalidator {
  constructor() {
    this.connector = new DebeziumConnector({
      bootstrapServers: 'localhost:9092',
      topic: 'dbserver1.inventory.users',
    });

    this.setupListener();
  }

  setupListener() {
    this.connector.on('change', async (event) => {
      const { op, before, after } = event;

      switch (op) {
        case 'u':  // Update
        case 'd':  // Delete
          const userId = before.id;
          await cache.del(`user:${userId}`);
          await cache.del(`user:${userId}:profile`);
          break;

        case 'c':  // Create
          const newUserId = after.id;
          // Optionally pre-warm cache
          await cache.set(`user:${newUserId}`, after, { ttl: 3600 });
          break;
      }
    });
  }
}
```

## Cache Tags and Grouping

### Hierarchical Tags

```javascript
class HierarchicalCache {
  constructor() {
    this.tagHierarchy = new Map();  // tag -> parent tags
  }

  defineTag(tag, parentTags = []) {
    this.tagHierarchy.set(tag, parentTags);
  }

  async set(key, value, options = {}) {
    const tags = options.tags || [];

    // Resolve all parent tags
    const allTags = new Set(tags);
    for (const tag of tags) {
      const parents = this.tagHierarchy.get(tag) || [];
      parents.forEach(parent => allTags.add(parent));
    }

    // Store with all tags
    await cache.set(key, value, { ...options, tags: Array.from(allTags) });
  }

  async invalidateTag(tag) {
    // Get all keys with this tag or its children
    const keys = await this.getKeysByTag(tag);
    await cache.del(...keys);
  }

  async getKeysByTag(tag) {
    const keys = new Set();

    // Direct tag matches
    const directKeys = await this.tagKeys.get(tag) || [];
    directKeys.forEach(key => keys.add(key));

    // Child tag matches
    for (const [childTag, parentTags] of this.tagHierarchy) {
      if (parentTags.includes(tag)) {
        const childKeys = await this.tagKeys.get(childTag) || [];
        childKeys.forEach(key => keys.add(key));
      }
    }

    return Array.from(keys);
  }
}

// Usage
const cache = new HierarchicalCache();

// Define tag hierarchy
cache.defineTag('user', ['data']);
cache.defineTag('post', ['data']);
cache.defineTag('feed', ['data', 'aggregated']);

// Cache with tags
await cache.set('user:123', userData, { tags: ['user'] });
await cache.set('post:456', postData, { tags: ['post'] });
await cache.set('feed:recent', feedData, { tags: ['feed'] });

// Invalidate all data
await cache.invalidateTag('data');
```

## Versioned Cache Keys

Include version in cache key to simplify invalidation.

```javascript
class VersionedCache {
  constructor() {
    this.versions = new Map();  // prefix -> version number
  }

  async get(prefix, key) {
    const version = this.getVersion(prefix);
    const versionedKey = `${prefix}:v${version}:${key}`;
    return await cache.get(versionedKey);
  }

  async set(prefix, key, value, options) {
    const version = this.getVersion(prefix);
    const versionedKey = `${prefix}:v${version}:${key}`;
    return await cache.set(versionedKey, value, options);
  }

  getVersion(prefix) {
    if (!this.versions.has(prefix)) {
      this.versions.set(prefix, 1);
    }
    return this.versions.get(prefix);
  }

  async invalidate(prefix) {
    // Increment version
    const currentVersion = this.getVersion(prefix);
    this.versions.set(prefix, currentVersion + 1);

    // Old keys will naturally expire
    // Optionally, delete old keys immediately
    await this.deleteOldVersionKeys(prefix, currentVersion);
  }

  async deleteOldVersionKeys(prefix, oldVersion) {
    const pattern = `${prefix}:v${oldVersion}:*`;
    const keys = await cache.keys(pattern);
    if (keys.length > 0) {
      await cache.del(...keys);
    }
  }
}

// Usage
const cache = new VersionedCache();

// Set cache
await cache.set('user', '123', userData, { ttl: 3600 });
await cache.get('user', '123');  // user:v1:123

// Invalidate all user caches
await cache.invalidate('user');

// New version
await cache.set('user', '123', userData, { ttl: 3600 });
await cache.get('user', '123');  // user:v2:123
```

## Cache Warming Strategies

### On-Demand Warming

Warm cache when first accessed.

```javascript
async function getWithWarmup(key, loader) {
  const cached = await cache.get(key);

  if (cached) {
    return cached;
  }

  // Cache miss - load and warm
  const value = await loader(key);
  await cache.set(key, value, { ttl: 3600 });

  // Warm related caches
  await warmRelatedCaches(value);

  return value;
}

async function warmRelatedCaches(user) {
  // Warm user's posts
  const posts = await db.posts.findByUserId(user.id);
  await cache.set(`user:${user.id}:posts`, posts, { ttl: 3600 });

  // Warm user's friends
  const friends = await db.friends.findByUserId(user.id);
  await cache.set(`user:${user.id}:friends`, friends, { ttl: 3600 });
}
```

### Scheduled Warming

Warm cache on a schedule.

```javascript
class CacheWarmer {
  constructor() {
    this.jobs = new Map();
  }

  schedule(key, loader, interval) {
    const job = setInterval(async () => {
      try {
        const value = await loader(key);
        await cache.set(key, value, { ttl: interval * 2 });
      } catch (error) {
        console.error('Cache warming failed:', error);
      }
    }, interval);

    this.jobs.set(key, job);

    // Initial warm
    loader(key).then(value => {
      cache.set(key, value, { ttl: interval * 2 });
    });
  }

  unschedule(key) {
    const job = this.jobs.get(key);
    if (job) {
      clearInterval(job);
      this.jobs.delete(key);
    }
  }
}

// Usage
const warmer = new CacheWarmer();

// Warm user cache every hour
warmer.schedule('user:123', async () => {
  return await db.users.findById(123);
}, 3600000);
```

### Predictive Warming

Warm cache based on access patterns.

```javascript
class PredictiveCacheWarmer {
  constructor() {
    this.accessPatterns = new Map();  // key -> access timestamps
    this.predictions = new Map();    // key -> next access prediction
  }

  recordAccess(key) {
    const now = Date.now();
    const timestamps = this.accessPatterns.get(key) || [];
    timestamps.push(now);

    // Keep only last 100 accesses
    if (timestamps.length > 100) {
      timestamps.shift();
    }

    this.accessPatterns.set(key, timestamps);
    this.updatePrediction(key);
  }

  updatePrediction(key) {
    const timestamps = this.accessPatterns.get(key);
    if (timestamps.length < 2) return;

    // Calculate average interval
    let totalInterval = 0;
    for (let i = 1; i < timestamps.length; i++) {
      totalInterval += timestamps[i] - timestamps[i - 1];
    }
    const avgInterval = totalInterval / (timestamps.length - 1);

    // Predict next access
    const lastAccess = timestamps[timestamps.length - 1];
    const predictedAccess = lastAccess + avgInterval;

    this.predictions.set(key, predictedAccess);

    // Schedule warmup before predicted access
    const warmupTime = predictedAccess - avgInterval * 0.1;  // 10% early
    const delay = warmupTime - Date.now();

    if (delay > 0 && delay < 3600000) {  // Within next hour
      setTimeout(async () => {
        await this.warmKey(key);
      }, delay);
    }
  }

  async warmKey(key) {
    const value = await this.loader(key);
    await cache.set(key, value, { ttl: 3600 });
  }
}
```

## Multi-Tier Caching (L1/L2)

### Two-Level Cache

```javascript
class TwoLevelCache {
  constructor(l1, l2) {
    this.l1 = l1;  // Fast, small cache (e.g., in-memory)
    this.l2 = l2;  // Slower, larger cache (e.g., Redis)
  }

  async get(key) {
    // Check L1 first
    const l1Value = await this.l1.get(key);
    if (l1Value) {
      return l1Value;
    }

    // Check L2
    const l2Value = await this.l2.get(key);
    if (l2Value) {
      // Promote to L1
      await this.l1.set(key, l2Value, { ttl: 300 });  // 5 minutes
      return l2Value;
    }

    return null;
  }

  async set(key, value, options = {}) {
    // Set in both levels
    await this.l1.set(key, value, { ttl: 300, ...options });
    await this.l2.set(key, value, options);
  }

  async invalidate(key) {
    await this.l1.del(key);
    await this.l2.del(key);
  }
}

// Usage
const l1 = new InMemoryCache({ maxSize: 1000 });
const l2 = new RedisCache();
const cache = new TwoLevelCache(l1, l2);
```

### Write-Through Multi-Tier

```javascript
class WriteThroughMultiTierCache {
  constructor(tiers) {
    this.tiers = tiers;  // [L1, L2, L3, ...]
  }

  async get(key) {
    for (const tier of this.tiers) {
      const value = await tier.get(key);
      if (value) {
        // Promote to higher tiers
        this.promote(key, value);
        return value;
      }
    }
    return null;
  }

  async set(key, value, options) {
    // Write to all tiers
    const promises = this.tiers.map(tier =>
      tier.set(key, value, options)
    );
    await Promise.all(promises);
  }

  async invalidate(key) {
    const promises = this.tiers.map(tier => tier.del(key));
    await Promise.all(promises);
  }

  async promote(key, value) {
    // Write to higher tiers only
    for (let i = 0; i < this.tiers.length - 1; i++) {
      await this.tiers[i].set(key, value, { ttl: 300 });
    }
  }
}
```

## CDN Cache Invalidation

### CDN Purge

```javascript
const CloudFront = require('aws-sdk/clients/cloudfront');

const cloudfront = new CloudFront({
  region: 'us-east-1',
});

async function invalidateCDN(paths) {
  const params = {
    DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
    InvalidationBatch: {
      CallerReference: Date.now().toString(),
      Paths: {
        Quantity: paths.length,
        Items: paths,
      },
    },
  };

  const result = await cloudfront.createInvalidation(params).promise();
  return result.Invalidation;
}

// Usage
await invalidateCDN(['/user/123', '/user/123/profile']);
```

### CDN Cache Tags

```javascript
// Set cache headers
app.get('/user/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);

  res.set('Cache-Control', 'public, max-age=3600');
  res.set('Cache-Tag', `user-${user.id},premium-${user.isPremium}`);

  res.json(user);
});

// Invalidate by tag
async function invalidateByTag(tag) {
  await cloudfront.createInvalidation({
    DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
    InvalidationBatch: {
      CallerReference: Date.now().toString(),
      Paths: {
        Quantity: 1,
        Items: [`*`],  // Invalidate all, filter by tag
      },
    },
  }).promise();
}
```

## Database Query Cache Invalidation

### Query-Based Invalidation

```javascript
class QueryCache {
  constructor() {
    this.queryDependencies = new Map();  // query -> affected tables
  }

  async query(sql, params, loader) {
    const cacheKey = this.getQueryKey(sql, params);

    const cached = await cache.get(cacheKey);
    if (cached) {
      return cached;
    }

    const result = await loader(sql, params);
    await cache.set(cacheKey, result, { ttl: 3600 });

    // Track dependencies
    this.trackDependencies(cacheKey, sql);

    return result;
  }

  trackDependencies(cacheKey, sql) {
    const tables = this.extractTables(sql);
    this.queryDependencies.set(cacheKey, tables);
  }

  extractTables(sql) {
    // Simple table extraction
    const matches = sql.match(/FROM\s+(\w+)/gi) || [];
    return matches.map(m => m.replace(/FROM\s+/i, '').toLowerCase());
  }

  async invalidateTable(table) {
    for (const [cacheKey, tables] of this.queryDependencies) {
      if (tables.includes(table)) {
        await cache.del(cacheKey);
      }
    }
  }
}

// Usage
const queryCache = new QueryCache();

async function getUsers() {
  return await queryCache.query(
    'SELECT * FROM users WHERE active = true',
    [],
    (sql, params) => db.query(sql, params)
  );
}

// Invalidate when users table changes
await queryCache.invalidateTable('users');
```

## Eventual Consistency Handling

### Version Vectors

```javascript
class VersionVectorCache {
  constructor() {
    this.versions = new Map();  // key -> { replicaId: version }
  }

  async get(key) {
    const cached = await cache.get(key);
    if (!cached) {
      return null;
    }

    // Check if this replica's version is up-to-date
    const myVersion = this.getVersion(key, this.replicaId);
    const cachedVersion = cached._version[this.replicaId];

    if (myVersion > cachedVersion) {
      // Our version is newer - cache is stale
      return null;
    }

    return cached._data;
  }

  async set(key, value) {
    // Increment our version
    this.incrementVersion(key, this.replicaId);

    const version = this.getVersion(key, this.replicaId);

    await cache.set(key, {
      _data: value,
      _version: this.getFullVersion(key),
    });
  }

  getVersion(key, replicaId) {
    const versions = this.versions.get(key) || {};
    return versions[replicaId] || 0;
  }

  incrementVersion(key, replicaId) {
    const versions = this.versions.get(key) || {};
    versions[replicaId] = (versions[replicaId] || 0) + 1;
    this.versions.set(key, versions);
  }
}
```

## Monitoring Cache Hit/Miss Rates

### Metrics Collection

```javascript
class CacheMetrics {
  constructor() {
    this.hits = 0;
    this.misses = 0;
    this.errors = 0;
  }

  recordHit() {
    this.hits++;
  }

  recordMiss() {
    this.misses++;
  }

  recordError() {
    this.errors++;
  }

  getHitRate() {
    const total = this.hits + this.misses;
    return total > 0 ? this.hits / total : 0;
  }

  getStats() {
    return {
      hits: this.hits,
      misses: this.misses,
      errors: this.errors,
      hitRate: this.getHitRate(),
      total: this.hits + this.misses,
    };
  }

  reset() {
    this.hits = 0;
    this.misses = 0;
    this.errors = 0;
  }
}

// Usage
const metrics = new CacheMetrics();

async function get(key) {
  const cached = await cache.get(key);

  if (cached) {
    metrics.recordHit();
    return cached;
  }

  metrics.recordMiss();
  const value = await loader(key);
  await cache.set(key, value);
  return value;
}

// Report metrics periodically
setInterval(() => {
  console.log('Cache stats:', metrics.getStats());
  metrics.reset();
}, 60000);
```

## Common Anti-Patterns

### 1. Cache-Aside Without Locking

```javascript
// Bad: Multiple requests can load same data
async function getUser(userId) {
  const cached = await cache.get(`user:${userId}`);
  if (cached) return cached;

  const user = await db.users.findById(userId);  // Multiple requests hit DB
  await cache.set(`user:${userId}`, user);
  return user;
}

// Good: Use locking or coalescing
async function getUser(userId) {
  return await coalescer.get(`user:${userId}`, async (key) => {
    const id = key.split(':')[1];
    return await db.users.findById(id);
  });
}
```

### 2. Inconsistent Invalidation

```javascript
// Bad: Update cache but not database
async function updateUser(userId, updates) {
  await cache.set(`user:${userId}`, updates);  // Cache updated
  // Forgot to update database!
}

// Good: Update database first, then invalidate cache
async function updateUser(userId, updates) {
  const user = await db.users.update(userId, updates);
  await cache.del(`user:${userId}`);
  return user;
}
```

### 3. No TTL

```javascript
// Bad: No expiration, cache never clears
await cache.set('user:123', userData);

// Good: Always set TTL
await cache.set('user:123', userData, { ttl: 3600 });
```

### 4. Caching Everything

```javascript
// Bad: Cache everything, including rarely accessed data
async function getData(key) {
  const cached = await cache.get(key);
  if (cached) return cached;

  const value = await db.get(key);
  await cache.set(key, value);
  return value;
}

// Good: Cache selectively based on access patterns
async function getData(key) {
  if (!shouldCache(key)) {
    return await db.get(key);
  }

  const cached = await cache.get(key);
  if (cached) return cached;

  const value = await db.get(key);
  await cache.set(key, value, { ttl: 3600 });
  return value;
}
```

## Best Practices

1. **Choose the Right Strategy**
   - TTL for slowly changing data
   - Event-driven for critical data
   - Write-through for read-heavy workloads
   - Cache-aside for general use

2. **Prevent Cache Stampedes**
   - Use locking or request coalescing
   - Implement background refresh
   - Consider soft purge for high-traffic keys

3. **Handle Failures Gracefully**
   - Always have fallback to database
   - Log cache errors
   - Implement circuit breakers

4. **Monitor and Adjust**
   - Track hit/miss rates
   - Monitor cache size
   - Adjust TTLs based on patterns

5. **Think About Consistency**
   - Understand your consistency requirements
   - Use appropriate invalidation strategies
   - Handle distributed scenarios carefully

## Related Skills

- [`04-database/redis-caching`](04-database/redis-caching/SKILL.md)
- [`13-file-storage/cdn-setup`](13-file-storage/cdn-setup/SKILL.md)
- [`47-performance-engineering/caching-strategies`](47-performance-engineering/caching-strategies/SKILL.md)
- [`04-database/connection-pooling`](04-database/connection-pooling/SKILL.md)

Overview

This skill presents practical cache invalidation strategies and patterns to keep cached data consistent while preserving performance. It summarizes common approaches—TTL, event-driven, write-through, write-behind, cache-aside, read-through—and higher-level invalidation tactics like purge, ban, tag-based invalidation, refresh, and soft purge. It focuses on trade-offs and implementation sketches you can apply to Python/Redis-style systems.

How this skill works

The skill inspects typical caching workflows and maps each to an invalidation strategy, showing when to invalidate, refresh, or serve stale data. It explains implementation primitives (TTL, locks, events, tag maps, background refresh) and gives small code patterns for acquiring locks, emitting invalidation events, and batching writes. It highlights stampede prevention and failure modes so you can choose the right trade-offs for consistency, latency, and durability.

When to use it

  • Use TTL (time-based) when data changes predictably and occasional staleness is acceptable.
  • Use event-driven invalidation when data updates must be reflected immediately across caches.
  • Use cache-aside for read-heavy workloads where only accessed items should be cached.
  • Use write-through when reads must always hit an up-to-date cache and write latency is acceptable.
  • Use write-behind to optimize write throughput when occasional eventual consistency is acceptable.
  • Use tag-based or pattern bans for complex, related key sets where single-key purges are insufficient.

Best practices

  • Prefer simple patterns first: TTL or cache-aside for most read-heavy services.
  • Combine strategies: TTL + event-driven invalidation reduces both stale windows and implementation complexity.
  • Avoid global deletes in high-traffic environments; prefer targeted bans, tags, or background refresh to limit spikes.
  • Implement stampede protection (locks or request coalescing) to avoid thundering-herd on cache misses.
  • Design idempotent invalidation events and retries; treat cache operations as best-effort with observability.
  • Measure and tune TTLs and refresh thresholds based on real change frequency and SLA requirements.

Example use cases

  • User profile caching: cache-aside with event-driven purge on profile updates and adaptive TTLs.
  • News feed / timeline: short TTL or soft purge with background refresh to serve stale content while revalidating.
  • Inventory or pricing: very short TTLs or event-driven invalidation to prevent stale pricing showing to customers.
  • High-write analytics: write-behind with batching to reduce DB load and periodic reconciliation to avoid data loss.
  • Multi-tenant feeds: tag-based invalidation to remove a tenant’s entire cache slice without touching others.

FAQ

How do I choose between TTL and event-driven invalidation?

Use TTL when simplicity and loose freshness bounds are fine; choose event-driven when updates must be visible immediately. Often combining both (short TTL + events) gives a robust compromise.

What prevents cache stampedes during a miss?

Use short-lived locks, request coalescing, or probabilistic early refresh (refresh when TTL low). Also add jitter and exponential backoff to retries to avoid synchronized retries.