Add caching: the Spring cache abstraction, keys, eviction timing and the self-invocation trap

A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 19 tests
producing 22 transcripts under docs/output/, plus 12 documentation chapters.

Findings the build pins:
- @EnableCaching has no exposeProxy attribute; the widely-copied
  @EnableCaching(exposeProxy = true) does not compile.
- Two methods sharing a cache name and an argument type share a key space, and one
  silently serves the other's answers.
- beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on
  7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept.
- Four of five invalid declarations start a clean context and throw at the first call.
- Caffeine on the classpath silently displaces the simple provider.
This commit is contained in:
2026-09-12 05:19:22 +00:00
parent 7e1676c763
commit 66208bcd97
76 changed files with 3710 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
[← diagnostics](11-diagnostics.md) · [README](../README.md)
# 12. Before this goes to production
## Should there be a cache here at all?
Be honest about the answer. A cache is a correctness liability you accept in exchange for
latency. If the method is not measurably slow, or the hit rate will be low, or the data must be
current, the right amount of caching is none. Half the caches in a typical codebase were added
without a measurement and are never revisited.
## The checklist
**Configuration**
- [ ] `spring.cache.type` is set explicitly, so a new dependency cannot change the provider
([chapter 8](08-providers-and-ttl.md))
- [ ] `spring.cache.cache-names` declares every cache, so a typo fails loudly
- [ ] Every mutable cache has a TTL. It bounds the damage from every other mistake on this list
- [ ] Every cache has a size bound, or the data set is provably small
- [ ] `@EnableCaching` is not on the main application class
**Correctness**
- [ ] One cache name per method, or an explicit `key` that includes the method
([chapter 4](04-keys.md))
- [ ] Keys are immutable and serialise to something stable
- [ ] Cached values are DTOs, not JPA entities ([chapter 9](09-versus-hibernate-l2.md))
- [ ] Cached values are immutable, or defensively copied — the map hands every caller the same
instance
- [ ] Mutating methods evict rather than put, with `beforeInvocation = true`
([chapter 5](05-eviction.md))
- [ ] Nothing relies on `this.cachedMethod(...)` ([chapter 3](03-self-invocation.md))
- [ ] Every cached method is called at least once by a test — four of the five invalid
declarations in [chapter 2](02-the-three-annotations.md) only fail at the first call
**Operations**
- [ ] Hit rate and eviction count are on a dashboard (`recordStats()` for Caffeine)
- [ ] There is a way to clear a cache without a deployment (`DELETE /actuator/caches/{name}`)
- [ ] A `CacheErrorHandler` decision has been made per cache, not inherited by accident
([chapter 5](05-eviction.md))
- [ ] The behaviour with `spring.cache.type=none` has been tried at least once
**Distributed caches only**
- [ ] Values are serializable and the format survives a rolling deployment — a changed DTO shape
with old entries still in Redis fails on read, per instance, at whatever hour
- [ ] `spring.cache.redis.key-prefix` keeps this application out of everyone else's keyspace
- [ ] The failure mode when the cache is unreachable has been decided: degrade or fail
- [ ] TTLs are short enough that a missed eviction self-corrects
## The one-line version
Give every cache a TTL, evict rather than put, cache DTOs, and set `spring.cache.type`. Those
four cover most of what goes wrong.