# Pathrule Pattern: Redis Caching (1.0.0)
# ::pathrule:package:redis-caching

### [RULE] Give every cached key a TTL  (path: /src/cache)
<!-- scope: folder | priority: high | strict -->

A key with no expiry never self-heals. If invalidation ever misses it, that value is wrong until something happens to overwrite it, which may be never.

- Set a TTL on every cached value (`SET key val EX seconds`, or `EXPIRE`). The TTL is your safety net: even if explicit invalidation fails, the data is wrong for at most one TTL.
- Choose the TTL from how stale the data may acceptably be, not a global default. Reference data tolerates minutes or hours; data a user just changed tolerates seconds.
- Do not treat Redis as durable storage. It is a cache: assume any key can vanish at any time (eviction, restart) and the code must fall back to the source of truth cleanly.
- Add small random jitter to TTLs of related keys so they do not all expire on the same second and stampede together (see the stampede rule).

---

### [RULE] Prevent cache stampedes on hot keys  (path: /src/cache)
<!-- scope: folder | priority: high | advisory -->

When a popular key expires, every concurrent request misses at the same instant and slams the database together. That stampede (also called a thundering herd) is how a cache expiry becomes a database outage.

- Serialize the recompute: on a miss for a hot key, let one request acquire a short lock (`SET key val NX EX`) and rebuild the value while the others briefly wait or serve the previous value. Only one query hits the database.
- Or recompute early: refresh a key before it expires (probabilistic early expiration / background refresh) so it is rarely cold at request time.
- Stagger expirations with TTL jitter so a batch of related keys does not expire on the same tick.
- Guard against cache penetration too: a flood of requests for keys that do not exist bypasses the cache entirely. Cache a short-lived negative result (or use a bloom filter) so missing keys do not hammer the database.

---

### [MEMORY] Cache-aside is the default read path  (path: /src/cache)

We use the cache-aside (lazy-loading) pattern as the default. The application, not Redis, owns the relationship between cache and database.

- Read path: check Redis; on a hit return it; on a miss read the database, write the value to Redis with a TTL, and return it. Only data that is actually requested gets cached.
- Because the app controls the fallback, a Redis outage degrades to reading the database directly rather than failing the request. Wrap cache reads so a cache error is logged and falls through, never throws to the user.
- Cache-aside pairs with cache invalidation on write (see the invalidation memory): the write path updates the database and then invalidates or refreshes the affected keys.
- Use write-through (write cache and DB together) only when you need the cache always warm and can accept the extra write latency; cache-aside is the simpler, more resilient default.

See /src/cache for the TTL and stampede rules and the invalidation and key-design memories.

---

### [MEMORY] Choose an invalidation strategy by required freshness  (path: /src/cache)

Cache invalidation is the hard problem; the trick is to match the strategy to how fresh the data must be rather than reaching for the most complex option everywhere.

- TTL-only: simplest and often enough. Let data refresh on its own schedule when a short lag is acceptable. No write-path coupling.
- Event-driven (write-path) invalidation: when a record changes, invalidate or update the specific keys derived from it. Use this when staleness is user-visible or incorrect. Invalidate the precise keys, not the whole cache.
- Tag / group invalidation: when one change should drop many related entries (a product update affecting many cached views), group keys under a tag and invalidate the group, rather than deleting entries one by one or flushing everything.
- Avoid the over-broad flush: dropping all of a record's cached data because one unrelated field changed wastes the cache. Scope invalidation to what actually changed.
- Prefer delete-then-recompute over update-in-place for derived values; recomputing from the source on the next read avoids subtle write-order races between cache and database.

See /src/cache for the cache-aside read path and the TTL rule.

---

### [MEMORY] Key design, serialization, and round-trips  (path: /src/cache)

The mechanics of how you store and fetch keys decide whether the cache is fast and debuggable or a mystery.

- Namespace keys with a stable convention (`entity:id:field`, e.g. `user:42:profile`) so keys are greppable, scopable for invalidation, and collision-free across features. Include a schema version segment so a shape change can invalidate a whole generation at once.
- Pick one serialization format (JSON for simplicity and debuggability; a compact binary format when size/latency matters) and apply it consistently. Store the minimal data the read path needs, not whole objects.
- Avoid sending one command per round-trip in a loop; that latency dominates. Use pipelining or `MGET`/multi-key commands to batch reads and writes into fewer round-trips.
- Do not use `KEYS` in production to find keys to invalidate; it blocks the server. Track the keys you need to invalidate (via tags/sets) or use `SCAN`.
- What not to cache: secrets, anything per-request and unique, data with hard real-time accuracy requirements, and values cheaper to recompute than to fetch from Redis.

See /src/cache for the cache-aside and invalidation memories.

---

### [SKILL] redis-cache-review  (path: /)

---
name: redis-cache-review
description: Review checklist for adding or changing a Redis cache. Run before merging any code that reads from or writes to a cache.
---

# Redis cache review

- [ ] Every cached key has a TTL chosen from acceptable staleness, with small jitter on related keys.
- [ ] Read path is cache-aside: miss falls back to the source of truth and repopulates with a TTL.
- [ ] A Redis error logs and falls through to the database; it never throws to the user.
- [ ] Hot keys are protected from stampede (lock/single-flight, early recompute, or staggered TTL).
- [ ] Requests for nonexistent keys cannot bypass the cache and hammer the DB (negative caching / bloom filter).
- [ ] Writes invalidate or refresh the precise affected keys; strategy (TTL-only / event-driven / tag) matches required freshness.
- [ ] Keys are namespaced and versioned (`entity:id:field`), with one consistent serialization format and minimal stored data.
- [ ] Multi-key access uses pipelining / MGET, not one command per round-trip; no `KEYS` in production.
- [ ] Nothing sensitive, per-request-unique, or hard-real-time is cached; Redis is treated as ephemeral, not durable.
