[← sync and async](07-sync-and-async.md) · [next: versus Hibernate L2 →](09-versus-hibernate-l2.md) # 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 walks a fixed order and stops at the first provider on the classpath: ``` 1 Generic 2 JCache 3 Hazelcast 4 Infinispan 5 Couchbase 6 Redis 7 Caffeine 8 Cache2k 9 Simple ``` 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.