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.
JOptimize Team
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.
| Scenario | Use |
|---|---|
| Cache accessed millions of times per second | Caffeine (no network) |
| Cache shared across multiple instances | Redis |
| Cache that survives app restart | Redis |
| Reduce DB load for single-instance app | Caffeine |
| Session-specific data | Caffeine (per-instance is fine) |
| User-specific data in multi-instance app | Redis |
Both: Caffeine for L1 (fast, per-instance), Redis for L2 (shared, durable).
<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; } }
@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()); }
@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() { } }
// 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).
@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 countcache.gets{result="miss"} — miss countcache.evictions — evictions per secondcache.load.duration — time to load on missA healthy cache shows > 90% hit rate. Below 70% usually means TTL is too short or cache size is too small.
maximumSize or TTL — without eviction, the cache grows unbounded until OOMnull results — by default, @Cacheable caches null; add unless = "#result == null" to avoid caching empty responses@CacheEvict without considering concurrency — between eviction and reload, concurrent requests cause multiple DB calls; use refreshAfterWrite for hot keysrecordStats() adds minimal overhead but is essential for sizing the cache correctly; always enable it in dev and stagingCaffeine 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.
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.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.