Hard20 minDistributed Systems
UpdatedAug 5, 2026
Edit

Cache invalidation and consistency

CONCEPTS:Cache InvalidationDistributed Caching

Question Variations

  • "Why does deleting a cache key after a database write still have races?"
  • "When is TTL-only invalidation acceptable?"
  • "How can versioned cache keys prevent stale overwrites?"

Why This Is Asked

Caching becomes difficult when the source of truth changes. Interviewers look for a stated consistency target, an update sequence that avoids stale overwrites, and a plan for invalidation delivery failures rather than a blanket claim that TTL solves freshness.

Key Concepts

  • Cache-aside: Read from cache, then source on a miss; invalidate after a successful source update.
  • TTL: Bounds staleness but does not guarantee immediate freshness.
  • Versioning: Versions or generation keys protect against out-of-order writes.
  • Failure handling: Invalidation events need retries, observability, and an expiry fallback.

Question Variations

  • “Why does deleting a cache key after a database write still have races?”
  • “When is TTL-only invalidation acceptable?”
  • “How can versioned cache keys prevent stale overwrites?”

Answers by Technology

+ Add Variant
System DesignImprove this answer ✏️

Expected Answer (Distributed Caching)

Start by defining the acceptable staleness for each read. In cache-aside, reads check the cache, load the database on a miss, then populate the cache. On a successful source-of-truth update, invalidate or replace the affected cache entries and retain a TTL as a backstop. TTL alone is suitable only when stale reads within its bound are acceptable. For stronger requirements, use explicit invalidation and make the source update and invalidation delivery observable and retryable.

There are races: a reader can miss, load an old value, while a writer updates the source and deletes the key; the reader can then write the old value back. Versioned values, generation keys, compare-and-set operations, or ordered event processing prevent older results from overwriting newer ones. Use an outbox or durable event stream when invalidation must survive application crashes, and ensure cache outages degrade to the source rather than blocking updates.

Why It Matters

An undefined freshness model produces hard-to-reproduce stale reads after writes. A concrete invalidation protocol makes consistency, latency, and failure trade-offs reviewable.

Example Code

interface CachedValue<T> { version: number; value: T; }

export async function writeIfNewer<T>(key: string, next: CachedValue<T>): Promise<void> {
  const current = await cache.get<CachedValue<T>>(key);
  if (!current || current.version <= next.version) {
    await cache.set(key, next, { ttlSeconds: 300 });
  }
}

Common Mistakes

  • Deleting the cache before the source write commits: A concurrent reader can repopulate old data, and a failed write leaves unnecessary cache loss.
  • Using TTL as a guarantee of freshness: It bounds age only after an entry is written and cannot react immediately to updates.

Follow-up Questions

  • What is a cache generation key? (Answer: A version included in cache lookup keys so a new generation naturally bypasses older entries.)
  • Why use an outbox for invalidation? (Answer: It records a source change durably so event publication can be retried after a crash.)

References