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.
caching
Companion project for The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators and the Self-Invocation Trap on ankurm.com.
Every number, key dump, stack trace and error message quoted in the article came out of
docs/output/, and every one of those files is regenerated by one script. Most of them are
produced by the test suite, so if a claim stops being true the build goes red.
Versions
| Spring Boot | 4.1.1 |
| Spring Framework | 7.0.9 |
| Caffeine | managed by Boot |
| JDK | 25 (Temurin 25.0.4.1+1) |
| Maven | 3.9 |
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
./scripts/run-all.sh # regenerates every file under docs/output/
Run the application on its own to poke at the diagnostics endpoint:
java -jar target/caching-1.0.0.jar --spring.cache.type=simple
curl -s localhost:8080/diag/warm
curl -s localhost:8080/diag/caches | jq .
Profiles
| Profile | What it changes |
|---|---|
| (none) | Boot's auto-detected provider. Caffeine is on the classpath, so that is what you get |
caffeine |
An explicit CaffeineCacheManager with expireAfterWrite=400ms, maximumSize=3, recordStats() and setAsyncCacheMode(true) |
txaware |
TransactionAwareCacheManagerProxy over ConcurrentMapCacheManager |
Most tests pin spring.cache.type=simple so the key dumps show a real ConcurrentHashMap.
Endpoints
| Endpoint | Purpose |
|---|---|
GET /diag/warm |
Calls a few cached methods so there is something to look at |
GET /diag/caches |
Every cache, every key, with the runtime class of key and value |
GET /actuator/caches |
Boot's own view: cache names and their manager |
GET /actuator/metrics/cache.gets |
Hit/miss counters, where the provider reports them |
/diag/* is a debugging aid with no authorisation. Delete it before shipping anything.
Documentation
| Chapter | Covers |
|---|---|
| 01 — What the cache abstraction is | the interceptor and the Cache interface; when not to cache |
| 02 — The annotations, attribute by attribute | every attribute from javap; five declarations that are rejected and when |
| 03 — The self-invocation trap | why this.method() caches nothing, three fixes ranked, non-public methods, @PostConstruct |
| 04 — Keys | SimpleKeyGenerator's three rules, the collision it makes easy, the SpEL surface, mutable keys |
| 05 — Eviction | beforeInvocation, what a thrown exception does, allEntries, CacheErrorHandler |
| 06 — Conditions and nulls | condition vs unless, NullValue, cache penetration |
07 — sync and async |
the stampede measured, the four sync=true restrictions, CompletableFuture |
| 08 — Providers and TTL | detection order, the dependency that silently changes your provider, per-provider properties |
| 09 — This is not the Hibernate L2 cache | three layers compared; why caching an entity gives you a shared detached object |
| 10 — Transactions | a rollback that does not roll the cache back; TransactionAwareCacheManagerProxy and a correction to a widely repeated claim |
| 11 — Diagnostics | four ways to see what the cache is doing |
| 12 — Before production | the checklist, and whether to cache at all |
Captured output
| File | What it shows |
|---|---|
01-basics.txt |
200 ms, then 0 ms, with the invocation counter |
02-put-evict-clear.txt |
the three annotations against one cache |
03-self-invocation.txt |
4 vs 2 repository calls, four call styles |
04-non-public-and-postconstruct.txt |
a protected method silently uncached; @EnableCaching's real attributes |
05-key-shapes.txt |
zero, one and two arguments in one map |
06-key-collision.txt |
one method serving another method's answer |
07-mutable-key.txt |
two entries, one unreachable |
08-evict-timing.txt |
a failed update leaving a stale entry |
09-conditions.txt |
condition vetoing before the call |
10-nulls.txt |
NullValue in the map |
11-stampede.txt |
16 invocations vs 1 |
12-async-return-types.txt |
CompletableFuture with async cache mode on |
13-transactions.txt |
the cache keeping a rolled-back value |
14-cached-entity.txt |
LazyInitializationException from a cached entity |
15-providers-and-ttl.txt |
Caffeine expiry, size bound and stats |
16-autoconfiguration.txt |
what Boot wired, and where the auto-configuration lives in Boot 4 |
17-async-cache-mode-missing.txt |
the runtime error a clean startup hides |
18-provider-detection.txt |
the provider nobody chose, and the detection order read out of the enum (it disagrees with the docs) |
19-transaction-aware.txt |
the deferred put, and the evict that is not deferred |
20-invalid-declarations.txt |
five rejected declarations, four of them at the first call |
22-decorator-bytecode.txt |
javap proving why beforeInvocation is not deferred |
23-diagnostics.txt |
every cache, every key, live |
Licence
MIT — see LICENSE.