Introduction: What is Caffeine Cache and Why it Matters
Caffeine is a high-performance, near-optimal caching library for Java developed by Google. It replaced Guava’s cache due to its superior efficiency and flexibility. Caffeine matters because it provides an in-memory cache with excellent hit rates, minimal overhead, and concurrent operations without locking, making it ideal for high-throughput, low-latency applications.
Core Concepts & Principles
Key Cache Terminology
- Cache: Temporary storage of data for faster future access
- Cache Hit: When requested data is found in cache
- Cache Miss: When requested data is not in cache
- Eviction: Process of removing entries from cache
- TTL (Time-to-Live): Maximum lifetime of a cache entry
- LRU (Least Recently Used): Eviction policy based on recent usage
Caffeine’s Core Features
- High-performance, memory-efficient design
- Window TinyLFU eviction policy (excellent hit rate)
- Concurrent operations without blocking
- Flexible loading strategies (synchronous, asynchronous)
- Rich statistics and monitoring
- Automatic entry expiration (time-based and reference-based)
Implementation: Getting Started
Maven Dependency
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Gradle Dependency
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
Basic Cache Creation
// Manual cache population
Cache<Key, Value> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
// Synchronous loading cache
LoadingCache<Key, Value> loadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build(key -> createValue(key));
// Asynchronous loading cache
AsyncLoadingCache<Key, Value> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.buildAsync(key -> createValueAsync(key));
Cache Configuration Options
Size-Based Eviction
// Maximum size (count of entries)
.maximumSize(10_000)
// Maximum weight (custom weighting)
.maximumWeight(10_000)
.weigher((key, value) -> value.getWeight())
Time-Based Expiration
// Expire after write
.expireAfterWrite(Duration.ofMinutes(10))
// Expire after access
.expireAfterAccess(Duration.ofMinutes(5))
// Variable expiration
.expireAfter(new Expiry<Key, Value>() {
public long expireAfterCreate(Key key, Value value, long currentTime) {
return timeUnit.toNanos(1);
}
public long expireAfterUpdate(Key key, Value value, long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Value value, long currentTime, long currentDuration) {
return currentDuration;
}
})
Reference-Based Eviction
// Weak keys (allows GC of keys)
.weakKeys()
// Weak values (allows GC of values)
.weakValues()
// Soft values (allows GC of values when memory is constrained)
.softValues()
Listeners & Stats
// Removal listener
.removalListener((key, value, cause) ->
System.out.printf("Key %s was removed due to %s\n", key, cause))
// Record stats
.recordStats()
Cache Operations
Basic Operations
// Manual cache
Cache<Key, Value> cache = Caffeine.newBuilder().build();
// Insert/update
cache.put(key, value);
// Retrieve (returns null if absent)
Value value = cache.getIfPresent(key);
// Get or compute
Value value = cache.get(key, k -> createValue(k));
// Invalidate
cache.invalidate(key);
// Bulk operations
cache.putAll(map);
cache.invalidateAll(keys);
cache.invalidateAll();
Loading Cache Operations
LoadingCache<Key, Value> cache = Caffeine.newBuilder()
.build(key -> createValue(key));
// Get (computes if absent)
Value value = cache.get(key);
// Get all (computes missing values)
Map<Key, Value> map = cache.getAll(keys);
// Refresh (asynchronously reload)
cache.refresh(key);
Async Cache Operations
AsyncLoadingCache<Key, Value> cache = Caffeine.newBuilder()
.buildAsync(key -> createValueAsync(key));
// Get as CompletableFuture
CompletableFuture<Value> future = cache.get(key);
// Get all as CompletableFuture
CompletableFuture<Map<Key, Value>> future = cache.getAll(keys);
Cache Statistics & Monitoring
Accessing Stats
Cache<Key, Value> cache = Caffeine.newBuilder()
.recordStats()
.build();
// Get stats
CacheStats stats = cache.stats();
// Access specific metrics
long hitCount = stats.hitCount();
long missCount = stats.missCount();
double hitRate = stats.hitRate();
long evictionCount = stats.evictionCount();
Key Stats Metrics
Metric | Description |
---|---|
hitCount() | Number of cache hits |
missCount() | Number of cache misses |
loadSuccessCount() | Number of successful loads |
loadFailureCount() | Number of failed loads |
totalLoadTime() | Total loading time (nanoseconds) |
evictionCount() | Number of evictions |
evictionWeight() | Weight of evicted entries |
hitRate() | Ratio of cache hits to requests |
averageLoadPenalty() | Average time to load a value |
Comparison of Cache Types
Feature | Manual Cache | Loading Cache | Async Loading Cache |
---|---|---|---|
Population | Manual | Automatic | Automatic |
Missing Value | Returns null | Computes value | Returns CompletableFuture |
Bulk Loading | No | Yes | Yes |
Refresh | No | Yes | Yes |
Execution | N/A | Blocking | Non-blocking |
Best for | Simple caching | Most use cases | High concurrency |
Common Challenges & Solutions
Challenge: Cache Stampede (Thundering Herd)
Problem: Multiple threads try to load the same key simultaneously
Solution:
- Use
LoadingCache
orAsyncLoadingCache
which handles concurrent loads - Consider
refreshAfterWrite()
to proactively refresh entries
Challenge: Memory Pressure
Problem: Cache consumes too much memory
Solution:
- Use
maximumSize()
ormaximumWeight()
- Consider
softValues()
for memory-sensitive caches - Monitor with
stats()
and adjust size accordingly
Challenge: Stale Data
Problem: Cache entries become outdated
Solution:
- Use
expireAfterWrite()
for time-based expiration - Use
refreshAfterWrite()
for background refresh - Implement
CacheLoader.reload()
for smart refreshing
Challenge: Cache Poisoning
Problem: Invalid values getting cached
Solution:
- Validate values before caching
- Use short TTLs for potentially volatile data
- Implement health checks with selective invalidation
Best Practices & Tips
Performance Optimization
- Size your cache appropriately (monitor hit/miss rates)
- Use asynchronous loading for slow data sources
- Consider preloading frequently accessed entries
- Warm up the cache before heavy traffic
Configuration Tips
- Set reasonable expiration times based on data volatility
- Use
weakKeys()
when keys have object identity semantics - Choose between
softValues()
andweakValues()
based on memory constraints - Favor
maximumSize()
overmaximumWeight()
unless weight varies significantly
Testing & Development
- Use
FakeTicker
for time-based testing - Enable
recordStats()
during development and testing - Consider a smaller cache size in tests for faster eviction
Spring Integration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "products");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return cacheManager;
}
}
Advanced Techniques
Custom Weigher
Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((String key, DataObject value) -> value.getSize())
.build();
Custom CacheLoader
CacheLoader<Key, Value> loader = new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) {
return getValueFromSource(key);
}
@Override
public Map<Key, Value> loadAll(Set<? extends Key> keys) {
return getMultipleValuesFromSource(keys);
}
@Override
public Value reload(Key key, Value oldValue) {
return refreshValue(key, oldValue);
}
};
Writer-Through Cache
Cache<Key, Value> cache = Caffeine.newBuilder()
.writer(new CacheWriter<Key, Value>() {
@Override
public void write(Key key, Value value) {
storage.put(key, value);
}
@Override
public void delete(Key key, Value value, RemovalCause cause) {
if (cause.wasEvicted()) {
storage.delete(key);
}
}
})
.build();
Resources for Further Learning
Official Documentation
Books & Articles
- “Java Performance: The Definitive Guide” by Scott Oaks
- “Caching Strategies and How to Choose the Right One”
- “Caffeine: High Performance Caching Library for Java”
Tools & Extensions
- Micrometer Integration for metrics
- Spring Cache Abstraction
- Dropwizard Metrics for monitoring
Community Resources
- Stack Overflow caffeine-cache tag
- Java Performance & Scalability community forums
- GitHub Discussions on the Caffeine repository