Caffeine Cache: Configuration, Policies, Stats
Fast recall
Quick revision
Core answer
@Cacheable. Production config needs four things: (1) bound size with maximumSize or maximumWeight — never unbounded; (2) expiry with expireAfterWrite or expireAfterAccess depending on freshness vs hit-rate priority; (3) `refreshAfterWrite` for async background refresh on hot keys; (4) `recordStats()` + Micrometer binding to monitor hit rate, eviction, load latency. Also set softValues() or heap weighting cautiously; prefer bounded eviction over GC pressure. Request coalescing (CacheLoader) prevents thundering herd for free.In-process caching with production-grade configuration
Unbounded cache; no stats; expireAfterAccess when expireAfterWrite fits; missing request coalescing; ignoring heap pressure
Discuss Window TinyLFU, async refresh, weigher for variable-size entries, Caffeine + Spring Cache integration, metric export
Interview-ready guide
Interview answer
Caffeine is the default in-process Java cache in 2026. It replaces Guava Cache (same author, Ben Manes) and uses Window TinyLFU admission policy for near-optimal hit rate.
What Caffeine adds over ConcurrentHashMap
- Size-bound eviction with Window TinyLFU
- Time-based expiry (expireAfterWrite, expireAfterAccess)
- Async loading and refresh
- Request coalescing (thundering herd free)
- Statistics for observability
- Weighted capacity for variable-size entries
Why Window TinyLFU over LRU
- LRU evicts least-recently-used; in workloads with scans or one-hit-wonders, LRU thrashes
- TinyLFU uses frequency sketches; admits only items likely to be re-accessed
- Protects hot data; near-optimal in most real workloads
Spring Boot configuration
- Dependencies: spring-boot-starter-cache + caffeine
- @EnableCaching plus CaffeineCacheManager with global Caffeine builder
- Or per-cache config with SimpleCacheManager for different sizes and TTLs
Production config needs four things
(1) Bound size
- maximumSize(N) by entry count
- maximumWeight(N) + weigher for variable-size entries
- Never unbounded
(2) Expiry
- expireAfterWrite: entry expires after d from write; freshness priority
- expireAfterAccess: expires after d from last access; hit-rate priority
- expireAfter(Expiry): per-entry custom TTL
- expireAfterWrite is the safer default; expireAfterAccess can keep garbage hot
(3) refreshAfterWrite for async background refresh
- After d, next read triggers async reload
- Old value served until reload completes
- Users never see misses on hot keys
- Stricter than expireAfterWrite
(4) recordStats() plus Micrometer binding
- Hit rate, eviction, load latency
- CaffeineCacheMetrics.monitor() for each cache
- Grafana alerts: hit ratio below 80%, eviction spike, load duration high
Usage with Spring @Cacheable
- @Cacheable(value = products, key = #id, sync = true)
- sync = true is critical: prevents thundering herd; coalesces concurrent requests
- @CacheEvict per key; avoid allEntries=true
Async patterns
- LoadingCache with refreshAfterWrite for hot data
- AsyncLoadingCache returning CompletableFuture for I/O loaders
Typical configurations
- Feature flags: 1k entries, 5 min TTL, 1 min refresh
- User profiles: 10k entries, 15 min TTL, 5 min refresh
- Product pages: 50k entries, 30 min TTL, 10 min refresh
- Session data: 50k entries, 30 min access-based
Sizing heuristics
- Size times avg entry bytes must fit comfortably in heap (30 to 50% headroom)
- Start at 10k entries; tune via hit rate metrics
- High eviction rate means cache too small
- Low hit rate means cache not useful or TTL too short
Observability — key metrics
- cache.size
- cache.gets (hit + miss)
- cache.evictions
- cache.hit.ratio
- cache.load.duration
- cache.load.failure
Trade-offs and gotchas
Per-instance state
- Caffeine is per-JVM
- 10-instance cluster means 10 independent copies
- Don't use for counters, quotas, cluster-wide truth — use Redis
Invalidation across instances
- Short TTL (accept stale)
- Redis pub/sub invalidation
- Event-driven invalidation (Kafka)
- CDC to invalidation events
GC impact
- Every entry is a Java object
- 100k entries of 10KB each equals about 1GB heap
- Bound conservatively; monitor GC pauses
Thundering herd without sync=true
- Concurrent cache-miss requests all call loader
- Always use sync=true for hot keys
- LoadingCache coalesces automatically
@CacheEvict(allEntries=true)
- Expensive; clears entire cache
- Prefer per-key eviction
Mutable keys break caching
- Use immutable key objects (records, Strings, Longs)
Big entries don't belong here
- 10MB entries go on disk or Redis
- Caffeine shines for millions of small entries
Cross-tenant cache leaks
- A @Cacheable method keyed by request params without tenant ID means cross-tenant leak
- Always include user or tenant in key or namespace the cache
Tiered caching — Caffeine L1 plus Redis L2
- Check Caffeine; on miss check Redis; on miss load DB
- Populate both on way up
- Publish invalidation event on writes
- Best of both worlds
Warm-up strategy
- Fresh JVM means empty cache; first minutes hit DB hard
- Warm at startup with @EventListener(ApplicationReadyEvent)
- Gradual rollout (canary) with LB slow ramp
Caffeine vs alternatives
- vs Guava Cache: Caffeine is faster, better hit rate, actively maintained
- vs EhCache: Caffeine simpler and faster; EhCache with Terracotta rarely chosen over Redis
- vs ConcurrentHashMap: just use Caffeine
The strategic answer
- Default in-process Java cache; superseded Guava
- Always bound; prefer expireAfterWrite; add refreshAfterWrite for hot data
- Enable recordStats and Micrometer binding
- Use sync=true to avoid thundering herd
- Per-instance only; use Redis for cluster-wide
- Combine as L1 with Redis L2 for high-traffic apps
- Watch heap pressure and GC
Caffeine Cache: Configuration, Policies, Stats
Direct Answer
Caffeine is the default in-process Java cache: it replaces Guava Cache, uses the Window TinyLFU admission policy (near-optimal hit rate), and integrates cleanly with Spring's @Cacheable. Production config needs four things: (1) bound size with maximumSize or maximumWeight — never unbounded; (2) expiry with expireAfterWrite or expireAfterAccess depending on freshness vs hit-rate priority; (3) refreshAfterWrite for async background refresh on hot keys; (4) recordStats() + Micrometer binding to monitor hit rate, eviction, load latency. Also set softValues() or heap weighting cautiously; prefer bounded eviction over GC pressure. Request coalescing (CacheLoader) prevents thundering herd for free.
Common Mistakes & Pitfalls
Unbounded cache; no stats; expireAfterAccess when expireAfterWrite fits; missing request coalescing; ignoring heap pressure
Check your understanding
Answer these without looking back at the article.
- How does Window TinyLFU differ from standard LRU and why is it better?
- How do you coordinate cache invalidation between Caffeine L1 and Redis L2?
- When would you use AsyncLoadingCache vs LoadingCache with sync=true?
- How do you size Caffeine cache with Micrometer metrics in production?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.