Files
spring-boot-demo/caching/docs/08-providers-and-ttl.md
T
asmhatre a9867c0423 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: 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.
2026-09-12 05:35:13 +00:00

4.7 KiB

← sync and async · next: versus Hibernate L2 →

8. Providers, TTL, and the dependency that changes your cache

The abstraction has no TTL

None. No time-to-live, no time-to-idle, no maximum size, no eviction policy. Those are provider features, and the reference documentation says so plainly.

On the default simple provider — a ConcurrentHashMap behind ConcurrentMapCache — an entry stays until something evicts it by hand or the process ends. That is fine for a lookup table with twelve rows and a slow leak for anything else.

With Caffeine configured for expireAfterWrite=400ms, maximumSize=3, docs/output/15-providers-and-ttl.txt:

two calls, same key, immediately -> 1 repository calls
one more call 600 ms later      -> 2 repository calls   <- the entry expired

five distinct keys written, maximumSize = 3
estimated size after eviction settles : 3
stats                                 : hits=1 misses=5 evictions=3

Auto-detection, and why your provider changed

If there is no CacheManager bean and no cacheResolver, Spring Boot imports one configuration per provider and the first one whose @ConditionalOnClass matches registers the manager; the rest back off on @ConditionalOnMissingBean.

The order is worth taking from the enum rather than from prose. CacheConfigurations holds an EnumMap<CacheType, String>, so the iteration order is the declaration order of org.springframework.boot.autoconfigure.cache.CacheType — which on 4.1.1 is:

1 GENERIC   2 JCACHE   3 HAZELCAST   4 COUCHBASE   5 INFINISPAN
6 REDIS     7 CACHE2K   8 CAFFEINE   9 SIMPLE   10 NONE

Spring Boot's reference page lists this as Generic, JCache, Hazelcast, Infinispan, Couchbase, Redis, Caffeine, Cache2k, Simple. Two pairs are swapped relative to what ships, and the second swap is the one that can bite: with both libraries on the classpath you get Cache2k, not Caffeine. ProviderDetectionTest reads CacheType.values() rather than transcribing a list, so it will notice if this changes again.

One more curiosity for anyone chasing Boot 4 package moves: the configurations live in org.springframework.boot.cache.autoconfigure, but CacheType itself stayed behind in org.springframework.boot.autoconfigure.cache, in spring-boot-autoconfigure.

That has a consequence worth internalising. This module added Caffeine because one chapter needed TTL. Every cache in the application moved off the simple provider as a result, and nothing said so — docs/output/18-provider-detection.txt catches it:

spring.cache.type          : (not set)
resolved CacheManager bean : org.springframework.cache.caffeine.CaffeineCacheManager

A transitive dependency on Hazelcast, added for something unrelated, will do the same thing and outrank Redis while it is at it. Set spring.cache.type explicitly in anything you deploy.

Per-provider configuration

Property Effect
spring.cache.type generic, jcache, hazelcast, infinispan, couchbase, redis, caffeine, cache2k, simple, none
spring.cache.cache-names Creates exactly these caches at startup; anything else fails at the call
spring.cache.caffeine.spec e.g. maximumSize=500,expireAfterAccess=600s
spring.cache.redis.time-to-live a Duration
spring.cache.redis.cache-null-values default true
spring.cache.redis.key-prefix, use-key-prefix keyspace hygiene in a shared Redis
spring.cache.jcache.provider, spring.cache.jcache.config JSR-107

spring.cache.type=none gives a NoOpCacheManager: every method runs every time, and the annotations stay where they are. Useful in tests, and the fastest way to answer "is the cache causing this?".

spring.cache.cache-names is worth using in production: it turns a typo in a cache name from a cache that silently never hits into an IllegalArgumentException at the first call.

One caveat — a single spring.cache.caffeine.spec applies to every cache. Per-cache expiry needs your own CaffeineCacheManager (or several CacheManager beans and cacheManager = "..." on the operations), which is another argument for one cache name per method.

Where the auto-configuration lives in Boot 4

org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration, shipped in the spring-boot-cache module. Boot 4 split spring-boot-autoconfigure into per-technology modules; the Boot 3 coordinate org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration no longer resolves. docs/output/16-autoconfiguration.txt checks both names against the running classpath. It matters if you write @ImportAutoConfiguration or exclusions by class name.