Stackbook Logo
performance

Cache Invalidation Strategy

Intent

Keep cached data acceptably fresh while preserving the performance benefit of caching.

Problem

Caches serve stale data when the source changes, and naive invalidation either misses updates or removes entries so aggressively that the cache is useless. Race conditions between readers and writers can repopulate a cache with old values after an invalidation. Without a deliberate strategy, correctness bugs appear only under load.

Forces

    Solution

    Cache Invalidation Strategy

    Caching trades freshness for speed. The strategy makes that trade explicit per data class instead of leaving it to defaults. Each class gets a staleness budget, an invalidation mechanism, and a defence against races and stampedes.

    How it works

    In cache-aside, the application reads the cache, falls back to the source on a miss, and stores the result with a time-to-live. On writes, the application updates the source and then deletes the cache entry, so the next read reloads fresh data. Event-driven invalidation publishes change events that every cache holder consumes. Versioned keys embed a version or content hash, so new data lands under a new key and old entries simply age out.

    Implementation steps

    1. Classify data: static, slowly changing, user-specific, and near-real-time. Set a maximum acceptable staleness for each.
    2. Choose the mechanism: time-to-live alone for tolerant data, delete-on-write for single-service ownership, event-driven for shared data, versioned keys for immutable content.
    3. Always set a time-to-live, even with event invalidation, as a safety net.
    4. Add jitter to expirations so many keys do not expire together.
    5. Coalesce concurrent misses for one key so only one request hits the source.
    6. Store a version with each value and refuse to overwrite a newer entry with an older one.
    7. Cache negative results briefly to protect the source from repeated misses.

    Common pitfalls

    • Updating the cache and the database in the wrong order, leaving a window for stale repopulation.
    • Invalidating on write but not on bulk imports or administrative changes.
    • A thundering herd after a cache restart or mass expiry.
    • Caching authorised responses under keys that omit the user or tenant.
    • Treating cache failure as fatal instead of degrading to the source.

    Metrics to watch

    • Hit ratio per data class.
    • Stale read rate where measurable through sampling.
    • Origin request rate on misses and during stampedes.
    • Invalidation event lag.
    • Cache latency percentiles and eviction rate.

    Example configuration

    cache:
      product-detail:
        pattern: cache-aside
        ttlSeconds: 300
        jitterPercent: 20
        invalidateOn: product.updated
        coalesceMisses: true
      price-list:
        pattern: versioned-key
        key: "prices:v{version}"
        ttlSeconds: 3600
      session-profile:
        pattern: write-through
        ttlSeconds: 60
        keyIncludes: [tenantId, userId]
    

    Practical rollout

    Before adding a cache, try to measure the staleness that users actually notice, because it is often larger than engineers assume. Start with the simplest mechanism, a time-to-live, and add event-driven invalidation only for classes where staleness causes real harm. Keep cache keys and policies in one reviewed configuration file, so changes are visible, and load test the cold-start path to confirm the origin survives an empty cache.

    Key takeaways

    • Define a staleness budget for every cached data class.
    • Delete on change, and always keep a time-to-live as a backstop.
    • Prevent stampedes with coalescing and jitter.
    • Use versions to defeat stale-overwrite races.

    ✓ When to Use

    • •Read-heavy data where the source of truth is slower or costlier
    • •Staleness tolerance differs between data classes
    • •Multiple services or regions share cached entities
    • •Cache stampedes or stale reads have caused incidents

    ✗ When Not to Use

    • •Data must always be perfectly current, such as account balances at transaction time
    • •Access is write-heavy with little reuse
    • •The source is already fast enough

    Pros

    • +Large reductions in latency and backend load
    • +Explicit freshness guarantees per data class
    • +Time-to-live bounds the damage of missed invalidations
    • +Versioned keys avoid race conditions entirely

    Cons

    • −Event-driven invalidation adds coupling and infrastructure
    • −Short time-to-live values reduce hit ratio
    • −Distributed caches complicate consistency across regions
    • −Hard to test for races

    Cost Profile

    Infrastructure

    Operational

    Cognitive

    Failure Modes

      Alternatives

        Related Patterns

          Competency Domains

          data statedistribution communicationrequirements constraints