# caching Companion project for **[The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators and the Self-Invocation Trap](https://ankurm.com/spring-cache-abstraction-cacheable-cacheevict-self-invocation-trap/)** on [ankurm.com](https://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 ```bash 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: ```bash 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](docs/01-what-caching-is.md) | the interceptor and the `Cache` interface; when not to cache | | [02 — The annotations, attribute by attribute](docs/02-the-three-annotations.md) | every attribute from `javap`; five declarations that are rejected and when | | [03 — The self-invocation trap](docs/03-self-invocation.md) | why `this.method()` caches nothing, three fixes ranked, non-public methods, `@PostConstruct` | | [04 — Keys](docs/04-keys.md) | `SimpleKeyGenerator`'s three rules, the collision it makes easy, the SpEL surface, mutable keys | | [05 — Eviction](docs/05-eviction.md) | `beforeInvocation`, what a thrown exception does, `allEntries`, `CacheErrorHandler` | | [06 — Conditions and nulls](docs/06-conditions-and-nulls.md) | `condition` vs `unless`, `NullValue`, cache penetration | | [07 — `sync` and async](docs/07-sync-and-async.md) | the stampede measured, the four `sync=true` restrictions, `CompletableFuture` | | [08 — Providers and TTL](docs/08-providers-and-ttl.md) | detection order, the dependency that silently changes your provider, per-provider properties | | [09 — This is not the Hibernate L2 cache](docs/09-versus-hibernate-l2.md) | three layers compared; why caching an entity gives you a shared detached object | | [10 — Transactions](docs/10-transactions.md) | a rollback that does not roll the cache back; `TransactionAwareCacheManagerProxy` and a correction to a widely repeated claim | | [11 — Diagnostics](docs/11-diagnostics.md) | four ways to see what the cache is doing | | [12 — Before production](docs/12-production-checklist.md) | the checklist, and whether to cache at all | ## Captured output | File | What it shows | |---|---| | [`01-basics.txt`](docs/output/01-basics.txt) | 200 ms, then 0 ms, with the invocation counter | | [`02-put-evict-clear.txt`](docs/output/02-put-evict-clear.txt) | the three annotations against one cache | | [`03-self-invocation.txt`](docs/output/03-self-invocation.txt) | 4 vs 2 repository calls, four call styles | | [`04-non-public-and-postconstruct.txt`](docs/output/04-non-public-and-postconstruct.txt) | a protected method silently uncached; `@EnableCaching`'s real attributes | | [`05-key-shapes.txt`](docs/output/05-key-shapes.txt) | zero, one and two arguments in one map | | [`06-key-collision.txt`](docs/output/06-key-collision.txt) | one method serving another method's answer | | [`07-mutable-key.txt`](docs/output/07-mutable-key.txt) | two entries, one unreachable | | [`08-evict-timing.txt`](docs/output/08-evict-timing.txt) | a failed update leaving a stale entry | | [`09-conditions.txt`](docs/output/09-conditions.txt) | `condition` vetoing before the call | | [`10-nulls.txt`](docs/output/10-nulls.txt) | `NullValue` in the map | | [`11-stampede.txt`](docs/output/11-stampede.txt) | 16 invocations vs 1 | | [`12-async-return-types.txt`](docs/output/12-async-return-types.txt) | `CompletableFuture` with async cache mode on | | [`13-transactions.txt`](docs/output/13-transactions.txt) | the cache keeping a rolled-back value | | [`14-cached-entity.txt`](docs/output/14-cached-entity.txt) | `LazyInitializationException` from a cached entity | | [`15-providers-and-ttl.txt`](docs/output/15-providers-and-ttl.txt) | Caffeine expiry, size bound and stats | | [`16-autoconfiguration.txt`](docs/output/16-autoconfiguration.txt) | what Boot wired, and where the auto-configuration lives in Boot 4 | | [`17-async-cache-mode-missing.txt`](docs/output/17-async-cache-mode-missing.txt) | the runtime error a clean startup hides | | [`18-provider-detection.txt`](docs/output/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`](docs/output/19-transaction-aware.txt) | the deferred put, and the evict that is not deferred | | [`20-invalid-declarations.txt`](docs/output/20-invalid-declarations.txt) | five rejected declarations, four of them at the first call | | [`22-decorator-bytecode.txt`](docs/output/22-decorator-bytecode.txt) | `javap` proving why `beforeInvocation` is not deferred | | [`23-diagnostics.txt`](docs/output/23-diagnostics.txt) | every cache, every key, live | ## Licence MIT — see [LICENSE](../LICENSE).