A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 20 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. - The documented cache-provider detection order does not match CacheType's enum order in 4.1.1: COUCHBASE before INFINISPAN, and CACHE2K before CAFFEINE. - 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.
2.7 KiB
2.7 KiB
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.typeis set explicitly, so a new dependency cannot change the provider (chapter 8)spring.cache.cache-namesdeclares 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
@EnableCachingis not on the main application class
Correctness
- One cache name per method, or an explicit
keythat includes the method (chapter 4) - Keys are immutable and serialise to something stable
- Cached values are DTOs, not JPA entities (chapter 9)
- 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) - Nothing relies on
this.cachedMethod(...)(chapter 3) - Every cached method is called at least once by a test — four of the five invalid declarations in chapter 2 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
CacheErrorHandlerdecision has been made per cache, not inherited by accident (chapter 5) - The behaviour with
spring.cache.type=nonehas 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-prefixkeeps 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.