Files
spring-boot-demo/caching/docs/01-what-caching-is.md
T
asmhatre 66208bcd97 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.
2026-09-12 05:19:22 +00:00

46 lines
2.5 KiB
Markdown

[← README](../README.md) · [next: the three annotations →](02-the-three-annotations.md)
# 1. What the Spring cache abstraction actually is
It is an interceptor and a map interface. That is the whole idea, and holding onto it explains
almost every surprise later.
When a bean carries `@Cacheable`, Spring does not modify the class. It places an AOP proxy in
front of it and puts a `CacheInterceptor` in the chain. On each call the interceptor:
1. asks a `KeyGenerator` for a key,
2. asks a `Cache` (looked up from a `CacheManager` by name) whether it holds that key,
3. returns the stored value if it does, and otherwise calls the real method and stores the result.
`org.springframework.cache.Cache` is a small interface — `get`, `put`, `evict`, `evictIfPresent`,
`clear`, `invalidate`, `retrieve`. Everything you associate with a cache product — expiry, size
limits, eviction policy, replication, persistence, statistics — lives behind that interface in a
provider. The abstraction itself has none of it. The reference documentation is explicit about
this in its "How can I set the TTL/TTI/eviction policy" section: you configure it on the provider.
## What that buys you
Portability of the *declaration*, not the behaviour. The same annotated method runs against a
`ConcurrentHashMap` in a unit test, Caffeine in one deployment and Redis in another, without the
service code changing. That is genuinely useful and it is the main reason to use it.
## When not to cache
- **The method is not slow.** A cache turns a 2 ms call into a 0.1 ms call and adds a correctness
problem. Measure first.
- **The data must be correct right now.** Balances, stock levels, permissions. A cache is a
deliberate decision to serve stale data; make it deliberately.
- **The hit rate will be low.** A cache keyed on something nearly unique — a search phrase, a
request id — is a memory leak wearing a performance costume.
- **The value is huge and the memory budget is not.** On the default provider nothing evicts.
`docs/output/01-basics.txt` has the smallest possible demonstration: 200 ms, then 0 ms, with the
repository's invocation counter proving the method body did not run the second time.
## The two caches people confuse
If you are using JPA, you already have caching whether you asked for it or not: the persistence
context (first level) and possibly Hibernate's second-level cache. They are a different thing
from this, at a different layer, with different failure modes.
[Chapter 9](09-versus-hibernate-l2.md) is the comparison.