Caffeine
GitHub Overview
ben-manes/caffeine
A high performance caching library for Java
Topics
Star History
Library
Caffeine
Overview
Caffeine is the highest performance local cache library for Java. It implements Window TinyLFU algorithm, async loading, automatic refresh, and size/time-based eviction features, serving as the default cache provider for Spring Boot 2.x and later. As of 2025, Caffeine has completely established itself as the local cache standard in the Java ecosystem, gaining overwhelming market share through performance that surpasses Guava Cache and standard adoption in Spring Boot. It has become an essential library in Java development with rapidly increasing adoption in microservices and high-load API servers.
Details
Caffeine 3.2.1 is actively developed as of 2025, providing optimization for Java 11+ and GraalVM Native Image support. The Window TinyLFU (W-TinyLFU) eviction algorithm achieves higher hit rates than traditional LRU algorithms, delivering 4% improvement. Async APIs avoid I/O blocking, with built-in bulk loading, automatic refresh, and statistics features. Full JCache (JSR-107) compliance enables usage with standard specifications.
Key Features
- Window TinyLFU Algorithm: High cache hit rates through optimal eviction strategy
- Async Loading: High throughput with non-blocking I/O
- Automatic Refresh: Background updates of expired data
- Size-based Eviction: Memory-efficient cache size management
- Time-based Eviction: Flexible expiration with writeAfter/accessAfter
- Statistics and Metrics: Performance monitoring and optimization support
Pros and Cons
Pros
- Best-in-class performance for Java local caching (several times faster than Guava Cache)
- Rich ecosystem integration through Spring Boot standard adoption
- High concurrency and scalability through async APIs
- 4% hit rate improvement over traditional algorithms with Window TinyLFU
- Memory-efficient design with reduced GC pressure
- Comprehensive statistics for operational visibility
Cons
- Local cache within single JVM (no distributed caching)
- Requires Java 11+ (constraints in legacy environments)
- Configuration complexity due to rich features (excessive for simple use cases)
- Memory usage proportional to cache size
- Cache warm-up time required
- Difficulty in understanding cache state during debugging
Reference Pages
Code Examples
Adding Dependencies
// Gradle setup
implementation 'com.github.ben-manes.caffeine:caffeine:3.2.1'
// Optional extensions
implementation 'com.github.ben-manes.caffeine:guava:3.2.1' // Guava compatibility
implementation 'com.github.ben-manes.caffeine:jcache:3.2.1' // JCache support
<!-- Maven setup -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.1</version>
</dependency>
Basic Cache Creation and Operations
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class BasicCacheExample {
public static void main(String[] args) {
// Basic cache creation
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1_000) // Maximum size
.expireAfterWrite(Duration.ofMinutes(5)) // Expire 5 minutes after write
.build();
// Cache operations
cache.put("user:1", "John Doe");
String user = cache.getIfPresent("user:1");
System.out.println("User: " + user); // "User: John Doe"
// Default value for missing keys
String defaultUser = cache.get("user:999", key -> "Unknown User");
System.out.println("Default: " + defaultUser); // "Default: Unknown User"
// Bulk operations
Map<String, String> users = Map.of(
"user:2", "Jane Smith",
"user:3", "Bob Johnson"
);
cache.putAll(users);
// Cache size and statistics
System.out.println("Cache size: " + cache.estimatedSize());
cache.invalidate("user:1"); // Individual removal
cache.invalidateAll(); // Clear all
}
}
LoadingCache (Automatic Loading)
import com.github.benmanes.caffeine.cache.LoadingCache;
public class LoadingCacheExample {
// Database access simulation
private static String loadUserFromDatabase(String userId) {
// Actual DB access logic
try {
Thread.sleep(100); // Simulate DB access time
return "User-" + userId + "-from-DB";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
public static void main(String[] args) {
// LoadingCache creation
LoadingCache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.refreshAfterWrite(Duration.ofMinutes(5)) // Background refresh after 5 minutes
.recordStats() // Record statistics
.build(key -> loadUserFromDatabase(key)); // Loader function
// Automatic loading
String user1 = userCache.get("user:123"); // Load from DB
String user2 = userCache.get("user:123"); // Get from cache
// Bulk loading
Map<String, String> users = userCache.getAll(
List.of("user:100", "user:101", "user:102")
);
// Check statistics
CacheStats stats = userCache.stats();
System.out.printf("Hit rate: %.2f%%\n", stats.hitRate() * 100);
System.out.printf("Miss count: %d\n", stats.missCount());
System.out.printf("Load time: %.2f ms\n", stats.averageLoadTime() / 1_000_000);
}
}
AsyncCache (Asynchronous Cache)
import com.github.benmanes.caffeine.cache.AsyncCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
public class AsyncCacheExample {
private static CompletableFuture<String> loadUserAsync(String userId) {
return CompletableFuture.supplyAsync(() -> {
// Async data retrieval
try {
Thread.sleep(50);
return "Async-User-" + userId;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
public static void main(String[] args) throws Exception {
// AsyncCache creation
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofMinutes(10))
.buildAsync();
// Async value retrieval
CompletableFuture<String> future1 = asyncCache.get("user:async1",
key -> loadUserAsync(key));
CompletableFuture<String> future2 = asyncCache.get("user:async2",
(key, executor) -> loadUserAsync(key));
// Get results
String result1 = future1.get();
String result2 = future2.get();
System.out.println("Async Result 1: " + result1);
System.out.println("Async Result 2: " + result2);
// Bulk async operations
CompletableFuture<Map<String, String>> bulkFuture = asyncCache.getAll(
List.of("user:bulk1", "user:bulk2"),
(keys, executor) -> {
Map<String, CompletableFuture<String>> futures = new HashMap<>();
for (String key : keys) {
futures.put(key, loadUserAsync(key));
}
return futures;
}
);
Map<String, String> bulkResults = bulkFuture.get();
System.out.println("Bulk Results: " + bulkResults);
}
}
Spring Boot Integration
// Spring Boot Configuration
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterAccess(Duration.ofMinutes(10))
.weakKeys()
.recordStats());
return cacheManager;
}
// Multiple cache configuration
@Bean
public CacheManager multipleCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
// User cache
manager.registerCustomCache("users",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build());
// Session cache
manager.registerCustomCache("sessions",
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(Duration.ofMinutes(30))
.build());
return manager;
}
}
// Service class usage
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User getUserById(String userId) {
// Retrieve user from database
return userRepository.findById(userId);
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// Clear all user cache
}
}
Custom Eviction Policy
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
public class CustomEvictionExample {
public static void main(String[] args) {
// Custom removal listener
RemovalListener<String, String> removalListener =
(key, value, cause) -> {
System.out.printf("Removed: %s=%s (cause: %s)\n",
key, value, cause);
// Cleanup processing
if (cause == RemovalCause.EXPIRED) {
// Processing for expiration
cleanupExpiredData(key, value);
} else if (cause == RemovalCause.SIZE) {
// Processing for size-based removal
logSizeBasedEviction(key);
}
};
// Weighted eviction
Cache<String, String> weightedCache = Caffeine.newBuilder()
.maximumWeight(1_000_000) // Maximum weight
.weigher((String key, String value) -> {
// Weight calculation based on object size
return key.length() + value.length();
})
.removalListener(removalListener)
.build();
// Time-based composite eviction
Cache<String, String> timeBasedCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(2)) // 2 hours after write
.expireAfterAccess(Duration.ofMinutes(30)) // 30 minutes after access
.refreshAfterWrite(Duration.ofMinutes(15)) // Refresh after 15 minutes
.removalListener(removalListener)
.build(key -> fetchDataFromSource(key));
// Automatic garbage collection integration with weak references
Cache<String, Object> weakRefCache = Caffeine.newBuilder()
.maximumSize(1000)
.weakKeys() // Weak reference for keys
.weakValues() // Weak reference for values
.build();
}
private static void cleanupExpiredData(String key, String value) {
// Cleanup process for expired data
System.out.println("Cleaning up expired data: " + key);
}
private static void logSizeBasedEviction(String key) {
// Log for size-based eviction
System.out.println("Size-based eviction: " + key);
}
private static String fetchDataFromSource(String key) {
// Retrieval process from data source
return "Fresh data for " + key;
}
}
Performance Monitoring and Optimization
public class CacheMonitoringExample {
public static void setupCacheWithMonitoring() {
Cache<String, String> monitoredCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats() // Enable statistics recording
.build();
// Periodic statistics report
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
CacheStats stats = monitoredCache.stats();
System.out.println("=== Cache Statistics ===");
System.out.printf("Hit Rate: %.2f%%\n", stats.hitRate() * 100);
System.out.printf("Miss Rate: %.2f%%\n", stats.missRate() * 100);
System.out.printf("Hit Count: %d\n", stats.hitCount());
System.out.printf("Miss Count: %d\n", stats.missCount());
System.out.printf("Load Count: %d\n", stats.loadCount());
System.out.printf("Eviction Count: %d\n", stats.evictionCount());
System.out.printf("Average Load Time: %.2f ms\n",
stats.averageLoadTime() / 1_000_000.0);
System.out.println("========================");
}, 0, 30, TimeUnit.SECONDS);
// JVM metrics integration (Micrometer example)
/*
MeterRegistry meterRegistry = new SimpleMeterRegistry();
CaffeineCacheMetrics.monitor(meterRegistry, monitoredCache, "userCache");
*/
}
// Benchmark and performance testing
public static void benchmarkCachePerformance() {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
int iterations = 100_000;
long startTime = System.nanoTime();
// Write benchmark
for (int i = 0; i < iterations; i++) {
cache.put(i, "Value " + i);
}
long writeTime = System.nanoTime() - startTime;
// Read benchmark
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
cache.getIfPresent(i % 1000);
}
long readTime = System.nanoTime() - startTime;
System.out.printf("Write time: %.2f ms\n", writeTime / 1_000_000.0);
System.out.printf("Read time: %.2f ms\n", readTime / 1_000_000.0);
System.out.printf("Write throughput: %.0f ops/sec\n",
iterations * 1_000_000_000.0 / writeTime);
System.out.printf("Read throughput: %.0f ops/sec\n",
iterations * 1_000_000_000.0 / readTime);
}
}