Skip to content
Spring Boot sb production 4 min read

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 keyEffect
maximumSize=1000Evict (LRU-like) once 1000 entries are exceeded
expireAfterWrite=10mEntry expires 10 minutes after it was written
expireAfterAccess=5mEntry expires 5 minutes after last read/write
refreshAfterWrite=1mAsynchronously reload after 1 minute (needs a CacheLoader)
weakKeys / softValuesGC-sensitive references
recordStatsEnable 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 CacheManager bean disables the property-based auto-configuration, so spring.cache.caffeine.spec is ignored once you take this route. Choose one approach — spec-based or programmatic — per application.

Caffeine vs ConcurrentMap vs Redis

FeatureConcurrentMap (default)CaffeineRedis
LocationIn-processIn-processExternal server
SpeedFastFastest (no serialization)Network round-trip
Size evictionNoYes (maximumSize)Yes (maxmemory)
TTL / time evictionNoYesYes
StatisticsNoYes (recordStats)Yes
Shared across instancesNoNoYes
Survives restartNoNoYes (with persistence)
Extra infrastructureNoneNoneRedis 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 expireAfterWrite for freshness guarantees; add expireAfterAccess to evict idle keys.
  • Use Caffeine for single-instance apps or per-node hot data; use Redis when instances must share state.
  • Enable recordStats and watch the hit rate before tuning sizes.
  • Don’t mix the spec property and a custom CacheManager bean — pick one configuration style.
Last updated June 13, 2026
Was this helpful?