---
name: caching_strategy
description: "Design caching layers that stay correct: choose invalidation patterns, set TTL discipline, and stop stampede incidents before they happen. Triggers on cache-aside, write-through, write-behind, TTL, invalidation, thundering herd, stale reads, or cache stampede questions."
---

# Caching Strategy

## Purpose

Pick the right caching pattern for a workload, keep cached data correct after writes, and keep a hot key from crushing the database when it expires. Use when adding a cache in front of a datastore, when cached data goes stale or wrong, or when a review flags a cache-correctness issue.

## Workflow

1. **Classify the data.** Read-heavy vs write-heavy? Predictable or spiky access? What staleness is tolerable, and what does a stale read cost (annoyance vs money)?
2. **Pick the read/write pattern** from the decision table below. Default to cache-aside with a TTL.
3. **Plan invalidation before shipping the cache.** For every write path, list every key that derives from the changed data. If you cannot list them all, use versioned keys or cache tags instead of deleting keys.
4. **Set TTL discipline.** Every key gets a TTL, even with event-driven invalidation (TTL catches missed events). Add jitter so keys do not expire in lockstep. See references/patterns.md for starting values by data type.
5. **Add stampede protection for hot keys.** At minimum: jittered TTLs. For high-traffic keys add request coalescing (single-flight) and/or probabilistic early refresh (XFetch).
6. **Verify in production.** Watch hit ratio, stale-read reports, and miss latency on the hottest keys for a week after shipping.

## Decision table

| Need | Pattern |
|---|---|
| General purpose, simple, resilient | **Cache-aside** (lazy): app reads cache, miss loads from DB and populates; writes update DB then delete the key |
| Simplify app code, cache owns loading | **Read-through**: cache library fetches on miss |
| Strong consistency, infrequent writes | **Write-through**: writes go to cache + DB synchronously |
| High write volume, acceptable loss window | **Write-behind**: write to cache, flush async; only with a durable queue between cache and DB |
| Predictable hot keys | **Refresh-ahead** or stale-while-revalidate |

## Output Contract

- A written pattern choice with the reason (read/write ratio, staleness tolerance, hot-key traffic).
- An invalidation map: every write path and every key it invalidates, or the versioning/tag scheme that replaces manual deletes.
- TTL per key family with jitter, plus which stampede defenses apply to the hottest keys.
- One worked example of the hot-key read path in the repo's language, tested against the real cache.

## Operating Rules

1. **Delete on write, never update in place.** On update/delete: update the DB, delete the cache key, let the next read repopulate. Updating the cached value races with concurrent readers repopulating it.
2. **Invalidate every derived key.** Entity key, list/index keys it appears in, computed rollups. Missing one is the bug. If the fan-out is too complex to enumerate, switch to versioned keys (`product:42:v3`, bump the version on write) or CDN cache tags.
3. **TTL is a safety net, not a strategy.** TTL-only invalidation is fine for data users tolerate stale. For anything user-facing, add explicit invalidation on write.
4. **Cross-service caches need a contract.** If service B caches data owned by service A, A publishes change events that B subscribes to, or B uses a short TTL and accepts staleness.
5. **Cache misses need a story too.** Cache "not found" with a short TTL (negative caching) so a missing key does not hammer the DB. Bulk loaders and single-item loaders must use the same lookup predicate, or real records get tombstoned.
6. **Single-flight is per process.** In-process request coalescing collapses concurrency on one server only. Ten servers still make ten backend calls. For fleet-wide stampede defense, use a distributed lock, probabilistic early refresh, or both.
