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

2.5 KiB

← README · next: the three annotations →

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 is the comparison.