Caffeine Cache
Caffeine is a high-performance, near-optimal Java caching library — the de facto successor to Guava’s cache. When you add it to a Spring Boot app, it plugs straight into the caching abstraction: the same @Cacheable / @CacheEvict annotations now sit on top of a fast in-process store that supports size limits and time-based eviction, neither of which the default ConcurrentMapCacheManager provides.
Why Caffeine over the default cache
Spring Boot’s default cache (ConcurrentMapCacheManager) is just a ConcurrentHashMap: it never evicts entries, so it grows unbounded and serves stale data forever. Caffeine fixes both problems while staying in the same JVM — no network hop, no extra server to run.
Adding the dependency
Spring Boot manages the Caffeine version, so no <version> is needed.
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
With Caffeine on the classpath and @EnableCaching present, Spring Boot auto-configures a CaffeineCacheManager and sets spring.cache.type to caffeine automatically.
Configuration with a cache spec
The simplest setup is fully declarative in application.yml. The spec string is parsed by Caffeine’s CaffeineSpec.
spring:
cache:
type: caffeine
cache-names: products, users
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m,recordStats
@Configuration
@EnableCaching
public class CacheConfig {
}
| Spec key | Effect |
|---|---|
maximumSize=1000 | Evict (LRU-like) once 1000 entries are exceeded |
expireAfterWrite=10m | Entry expires 10 minutes after it was written |
expireAfterAccess=5m | Entry expires 5 minutes after last read/write |
refreshAfterWrite=1m | Asynchronously reload after 1 minute (needs a CacheLoader) |
weakKeys / softValues | GC-sensitive references |
recordStats | Enable hit/miss statistics |
These settings apply to every cache named in cache-names. Once you set the type to Caffeine, the same @Cacheable code from the caching page works unchanged:
@Cacheable("products")
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
}
Per-cache configuration
A single global spec rarely fits every cache — a short-lived tokens cache and a long-lived countries cache need different rules. Build a CaffeineCacheManager programmatically and register named caches with their own Caffeine builders.
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.*;
import java.time.Duration;
@Configuration
@EnableCaching
public class CaffeineConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
// Default for any cache not explicitly registered
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(Duration.ofMinutes(5)));
// Per-cache override: short TTL for auth tokens
manager.registerCustomCache("tokens", Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(60))
.build());
// Per-cache override: large, long-lived reference data
manager.registerCustomCache("countries", Caffeine.newBuilder()
.maximumSize(300)
.expireAfterWrite(Duration.ofHours(24))
.build());
return manager;
}
}
Note: Defining your own
CacheManagerbean disables the property-based auto-configuration, sospring.cache.caffeine.specis ignored once you take this route. Choose one approach — spec-based or programmatic — per application.
Caffeine vs ConcurrentMap vs Redis
| Feature | ConcurrentMap (default) | Caffeine | Redis |
|---|---|---|---|
| Location | In-process | In-process | External server |
| Speed | Fast | Fastest (no serialization) | Network round-trip |
| Size eviction | No | Yes (maximumSize) | Yes (maxmemory) |
| TTL / time eviction | No | Yes | Yes |
| Statistics | No | Yes (recordStats) | Yes |
| Shared across instances | No | No | Yes |
| Survives restart | No | No | Yes (with persistence) |
| Extra infrastructure | None | None | Redis server |
The decisive question is shared state. Caffeine lives inside one JVM, so in a multi-instance deployment each node has its own independent cache — fine for read-heavy reference data, but unsuitable when every node must see the same value immediately. For that, use a distributed cache: see Redis Caching.
Tip: Caffeine and Redis are not mutually exclusive. A common pattern is a two-tier (near) cache: Caffeine as a tiny L1 in each instance to absorb the hottest keys, Redis as the shared L2 source of truth.
Verifying eviction
With recordStats enabled you can confirm Caffeine is doing its job. Expose stats via Actuator metrics (cache.gets, cache.evictions) once you bind the cache to a MeterRegistry.
products cache size=1000 (max) evictions rising as new keys arrive
tokens cache entries expire ~60s after write
hitRate=0.92 most reads served from cache
Best Practices
- Always set
maximumSize(and usually a TTL) — an unbounded cache is a memory leak waiting to happen. - Prefer
expireAfterWritefor freshness guarantees; addexpireAfterAccessto evict idle keys. - Use Caffeine for single-instance apps or per-node hot data; use Redis when instances must share state.
- Enable
recordStatsand watch the hit rate before tuning sizes. - Don’t mix the spec property and a custom
CacheManagerbean — pick one configuration style.