Back to Blog
spring-bootcachecaffeineperformancejavaoptimization

Spring Boot Caffeine Cache: The Fastest In-Process Cache for Java (2026)

Caffeine is the fastest Java in-memory cache — significantly faster than Guava Cache, EHCache 2.x, and Spring's default ConcurrentMapCache. Learn how to configure it in Spring Boot.

J

JOptimize Team

May 28, 2026· 7 min read

When you need fast in-process caching — no network hop, microsecond latency — Caffeine is the answer. It beats Guava's LoadingCache by 2-3x in throughput benchmarks and uses a Window TinyLFU eviction policy that achieves near-optimal hit rates. Spring Boot integrates with it out of the box.


When Caffeine vs Redis

ScenarioUse
Cache accessed millions of times per secondCaffeine (no network)
Cache shared across multiple instancesRedis
Cache that survives app restartRedis
Reduce DB load for single-instance appCaffeine
Session-specific dataCaffeine (per-instance is fine)
User-specific data in multi-instance appRedis

Both: Caffeine for L1 (fast, per-instance), Redis for L2 (shared, durable).


Setup

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); // Default spec for all caches manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(10)) .recordStats()); // Enable hit rate metrics return manager; } }

Per-Cache Configuration (Different TTLs)

@Bean public CacheManager cacheManager() { Map<String, CaffeineCache> caches = new HashMap<>(); // Products — large, stable, cache 1 hour caches.put("products", buildCache("products", Caffeine.newBuilder() .maximumSize(5000) .expireAfterWrite(Duration.ofHours(1)) .recordStats())); // User sessions — smaller, expire on access caches.put("userSessions", buildCache("userSessions", Caffeine.newBuilder() .maximumSize(10000) .expireAfterAccess(Duration.ofMinutes(30)) // Expire if not used for 30min .recordStats())); // Exchange rates — tiny, refresh frequently caches.put("exchangeRates", buildCache("exchangeRates", Caffeine.newBuilder() .maximumSize(50) .refreshAfterWrite(Duration.ofMinutes(1)) // Async refresh — no stale reads .expireAfterWrite(Duration.ofMinutes(5)) .recordStats())); SimpleCacheManager manager = new SimpleCacheManager(); manager.setCaches(caches.values()); return manager; } private CaffeineCache buildCache(String name, Caffeine<Object, Object> caffeine) { return new CaffeineCache(name, caffeine.build()); }

Usage: @Cacheable

@Service @RequiredArgsConstructor public class ProductService { @Cacheable(value = "products", key = "#id") public ProductDto findById(Long id) { return productRepository.findById(id) .map(ProductMapper::toDto) .orElseThrow(() -> new ProductNotFoundException(id)); } @Cacheable(value = "products", key = "#category + ':' + #pageable.pageNumber", condition = "#pageable.pageSize <= 20") // Only cache small pages public Page<ProductDto> findByCategory(String category, Pageable pageable) { return productRepository.findByCategory(category, pageable).map(ProductMapper::toDto); } @CacheEvict(value = "products", key = "#product.id") public ProductDto update(ProductDto product) { return ProductMapper.toDto(productRepository.save(ProductMapper.toEntity(product))); } @CacheEvict(value = "products", allEntries = true) // Clear entire cache public void clearProductCache() { } }

refreshAfterWrite vs expireAfterWrite

// expireAfterWrite — entry is REMOVED after TTL // → Next request after expiry: cache miss, blocking DB call Caffeine.newBuilder().expireAfterWrite(Duration.ofMinutes(5)) // refreshAfterWrite — entry is REFRESHED asynchronously after TTL // → Next request after TTL: returns STALE value, triggers async reload // → No blocking cache miss for hot keys Caffeine.newBuilder() .refreshAfterWrite(Duration.ofMinutes(5)) .buildAsync(key -> productRepository.findById((Long) key).orElse(null));

refreshAfterWrite is ideal for data that must always be fast to read but can tolerate brief staleness (exchange rates, configuration, reference data).


Cache Statistics and Monitoring

@Component @RequiredArgsConstructor public class CacheMetricsExporter { private final CacheManager cacheManager; private final MeterRegistry meterRegistry; @PostConstruct public void bindMetrics() { cacheManager.getCacheNames().forEach(cacheName -> { CaffeineCache caffeineCache = (CaffeineCache) cacheManager.getCache(cacheName); com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache = caffeineCache.getNativeCache(); // Bind Caffeine stats to Micrometer CacheMetrics.monitor(meterRegistry, nativeCache, cacheName, List.of()); }); } }

This exposes to Prometheus/Grafana:

  • cache.gets{result="hit"} — hit count
  • cache.gets{result="miss"} — miss count
  • cache.evictions — evictions per second
  • cache.load.duration — time to load on miss

A healthy cache shows > 90% hit rate. Below 70% usually means TTL is too short or cache size is too small.


Common Mistakes to Avoid

  • No maximumSize or TTL — without eviction, the cache grows unbounded until OOM
  • Caching null results — by default, @Cacheable caches null; add unless = "#result == null" to avoid caching empty responses
  • Using @CacheEvict without considering concurrency — between eviction and reload, concurrent requests cause multiple DB calls; use refreshAfterWrite for hot keys
  • Not recording stats in devrecordStats() adds minimal overhead but is essential for sizing the cache correctly; always enable it in dev and staging

Summary

Caffeine is the fastest in-process Java cache, with Window TinyLFU eviction and near-optimal hit rates. Configure per-cache TTLs (expireAfterWrite for correctness, refreshAfterWrite for zero-latency hot paths), always set maximumSize, and monitor hit rate via Micrometer. Use Caffeine as L1 for single-instance apps or as a local cache in front of Redis for multi-instance deployments.


Detect Cache Configuration Issues

JOptimize flags unbounded Caffeine caches without TTL, missing unless = "#result == null" on nullable methods, and caching JPA entities directly (lazy load risks).

Cache smarter with the fastest Java cache — free scan.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.