Caffeine
High-performance in-application caching library for Java. Successor to Guava Cache with optimized algorithms and rich features.
Caffeine
Caffeine is a high-performance in-application caching library for Java. Designed as the successor to Guava Cache, it provides optimized algorithms and rich features, serving as the default cache provider for Spring Boot 2.0 and later.
Overview
Caffeine is positioned as the most recommended local cache solution for Java developers. It's a modern caching library specifically designed for Java 8+ that leverages the experience gained from Google Guava Cache design. With excellent hit rates and low latency, combined with superior integration in Spring Boot environments, its adoption is rapidly expanding in enterprise Java applications.
Features
Key Capabilities
- High-Performance Algorithms: Excellent cache hit rates through Window TinyLFU algorithm
- Size-Based Eviction: Flexible cache size management based on entry count or weight functions
- Time-Based Expiration: Automatic deletion based on time since write or access
- Automatic Refresh: Asynchronous data update functionality in the background
- Spring Integration: Seamless integration with Spring Boot/Spring Framework
- Statistics: Detailed cache performance statistics collection
Architecture
Core design principles of Caffeine:
- Non-Blocking: Improved throughput through asynchronous processing in read operations
- Near-Optimal: Superior eviction decisions through W-TinyLFU algorithm compared to Guava
- Java 8+ Optimization: Leveraging Stream API, Optional, and CompletableFuture
- Memory Efficiency: JVM heap optimized design without requiring off-heap storage
Performance Characteristics
- Up to 3x write performance improvement compared to Guava Cache
- Excellent throughput in read-only workloads
- Reduced garbage collection load
- Support for both synchronous and asynchronous operation modes
Pros and Cons
Advantages
- Excellent Performance: Market-leading Java local cache performance
- Spring Boot Standard: Easy integration as default cache provider since 2.0
- Rich Features: Comprehensive functionality including size/time/manual eviction, statistics, refresh
- Superior Hit Rate: Efficient cache management through Window TinyLFU algorithm
- Active Development: Continuous performance improvements and bug fixes
- Lightweight Dependencies: Minimal external dependencies
Disadvantages
- Local Only: JVM local only, not a distributed cache
- Java 8+ Required: Cannot be used with older Java versions
- Memory Constraints: Cache capacity dependent on JVM heap size
- Configuration Complexity: Advanced configuration requires detailed understanding
- No Persistence: Cache data lost on application restart
- Distributed Environments: No consistency guarantees in cluster environments
References
Code Examples
Adding Dependencies and Setup
<!-- Maven dependency -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Spring Boot Cache Starter (auto-detects Caffeine) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
// Gradle dependency
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
// Spring Boot Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
Basic Cache Operations
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
public class CaffeineBasicExample {
public static void main(String[] args) {
// Create basic cache
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1000) // Maximum 1000 entries
.expireAfterWrite(Duration.ofMinutes(10)) // Expire 10 minutes after write
.build();
// Insert data
cache.put("key1", "value1");
cache.put("key2", "value2");
// Retrieve data
String value1 = cache.getIfPresent("key1");
System.out.println("key1: " + value1);
// Generate value if key doesn't exist
String value3 = cache.get("key3", key -> {
// Simulate database or API retrieval
return "generated_value_for_" + key;
});
System.out.println("key3: " + value3);
// Batch operations
Map<String, String> data = Map.of(
"batch1", "bvalue1",
"batch2", "bvalue2",
"batch3", "bvalue3"
);
cache.putAll(data);
// Check cache statistics
System.out.println("Cache size: " + cache.estimatedSize());
// Manual removal
cache.invalidate("key1");
cache.invalidateAll();
}
}
Using LoadingCache (Automatic Loading)
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.CacheLoader;
public class CaffeineLoadingExample {
// Simulate data source
private static final Map<String, String> DATABASE = Map.of(
"user:1", "John Doe",
"user:2", "Jane Smith",
"user:3", "Bob Wilson"
);
public static void main(String[] args) {
// Create LoadingCache
LoadingCache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterAccess(Duration.ofMinutes(5)) // Expire 5 minutes after access
.refreshAfterWrite(Duration.ofMinutes(1)) // Refresh 1 minute after write
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// Simulate database loading
System.out.println("Loading from database: " + key);
Thread.sleep(100); // Simulate database access delay
return DATABASE.getOrDefault(key, "Unknown User");
}
@Override
public String reload(String key, String oldValue) throws Exception {
// Asynchronous refresh processing
System.out.println("Refreshing: " + key);
return load(key);
}
});
// Retrieve data (CacheLoader is automatically called)
String user1 = userCache.get("user:1");
String user2 = userCache.get("user:2");
String unknown = userCache.get("user:999");
System.out.println("User 1: " + user1);
System.out.println("User 2: " + user2);
System.out.println("Unknown: " + unknown);
// Batch loading
Map<String, String> users = userCache.getAll(
Arrays.asList("user:1", "user:2", "user:3")
);
users.forEach((k, v) -> System.out.println(k + " -> " + v));
}
}
AsyncCache (Asynchronous Cache)
import com.github.benmanes.caffeine.cache.AsyncCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CaffeineAsyncExample {
private static final ExecutorService executor = Executors.newFixedThreadPool(4);
public static void main(String[] args) {
// Create asynchronous cache
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.executor(executor) // Custom executor
.buildAsync();
// Asynchronous value setting
CompletableFuture<Void> putFuture = asyncCache.put("async_key",
CompletableFuture.supplyAsync(() -> {
// Simulate heavy processing
try {
Thread.sleep(500);
return "async_value_generated_at_" + System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, executor));
// Asynchronous value retrieval
CompletableFuture<String> getFuture = asyncCache.get("async_key",
(key, executor) -> CompletableFuture.supplyAsync(() -> {
System.out.println("Generating value for: " + key);
return "default_value_for_" + key;
}, executor));
// Get results
getFuture.thenAccept(value -> {
System.out.println("Async result: " + value);
}).join();
// Multiple asynchronous operations
CompletableFuture<Map<String, String>> batchFuture = asyncCache.getAll(
Arrays.asList("key1", "key2", "key3"),
(keys, executor) -> {
Map<String, CompletableFuture<String>> futures = keys.stream()
.collect(Collectors.toMap(
key -> key,
key -> CompletableFuture.supplyAsync(() ->
"batch_value_" + key, executor)
));
return futures;
}
);
batchFuture.thenAccept(results -> {
results.forEach((k, v) -> System.out.println("Batch: " + k + " -> " + v));
}).join();
executor.shutdown();
}
}
Spring Boot Integration
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@SpringBootApplication
@EnableCaching
public class CaffeineSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(CaffeineSpringBootApplication.class, args);
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User findUser(Long userId) {
// Simulate database access
System.out.println("Fetching user from database: " + userId);
simulateSlowDatabase();
return new User(userId, "User " + userId, "user" + userId + "@example.com");
}
@CacheEvict(value = "users", key = "#userId")
public void updateUser(Long userId, User user) {
System.out.println("Updating user: " + userId);
// Cache entry will be removed
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
System.out.println("Clearing all user cache");
}
private void simulateSlowDatabase() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// application.yml configuration
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterAccess=300s,recordStats
cache-names:
- users
- products
- orders
# Custom configuration (application.properties)
# spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=600s
Custom Cache Configuration
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.Weigher;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
public class CaffeineAdvancedExample {
public static void main(String[] args) {
// Advanced cache configuration
Cache<String, String> advancedCache = Caffeine.newBuilder()
.maximumWeight(1000) // Weight-based limit
.weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return key.length() + value.length();
}
})
.expireAfterAccess(Duration.ofMinutes(5))
.expireAfterWrite(Duration.ofMinutes(10))
.refreshAfterWrite(Duration.ofMinutes(1))
.recordStats() // Record statistics
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(String key, String value, RemovalCause cause) {
System.out.println("Removed: " + key + " -> " + value + " (" + cause + ")");
}
})
.build();
// Add data and access
advancedCache.put("short", "val");
advancedCache.put("medium_key", "medium_value");
advancedCache.put("very_long_key_name", "very_long_value_content_here");
// Get statistics
CacheStats stats = advancedCache.stats();
System.out.println("Request count: " + stats.requestCount());
System.out.println("Hit count: " + stats.hitCount());
System.out.println("Miss count: " + stats.missCount());
System.out.println("Hit rate: " + stats.hitRate());
System.out.println("Average load penalty: " + stats.averageLoadPenalty());
System.out.println("Eviction count: " + stats.evictionCount());
// Cache cleanup
advancedCache.cleanUp();
// Add more data to test weight limits
for (int i = 0; i < 100; i++) {
advancedCache.put("key" + i, "value" + i);
}
System.out.println("Final cache size: " + advancedCache.estimatedSize());
}
}
Performance Testing and Comparison
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class CaffeinePerformanceTest {
private static final int OPERATIONS = 1_000_000;
private static final int THREADS = 4;
public static void main(String[] args) throws InterruptedException {
testCaffeine();
testConcurrentHashMap();
}
private static void testCaffeine() throws InterruptedException {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10000)
.recordStats()
.build();
long startTime = System.nanoTime();
Thread[] threads = new Thread[THREADS];
for (int t = 0; t < THREADS; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < OPERATIONS / THREADS; i++) {
int key = ThreadLocalRandom.current().nextInt(1000);
cache.get(key, k -> "value" + k);
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long duration = System.nanoTime() - startTime;
CacheStats stats = cache.stats();
System.out.println("=== Caffeine Performance ===");
System.out.println("Duration: " + duration / 1_000_000 + " ms");
System.out.println("Hit rate: " + String.format("%.2f%%", stats.hitRate() * 100));
System.out.println("Average load time: " + stats.averageLoadPenalty() + " ns");
}
private static void testConcurrentHashMap() throws InterruptedException {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
long startTime = System.nanoTime();
Thread[] threads = new Thread[THREADS];
for (int t = 0; t < THREADS; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < OPERATIONS / THREADS; i++) {
int key = ThreadLocalRandom.current().nextInt(1000);
map.computeIfAbsent(key, k -> "value" + k);
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long duration = System.nanoTime() - startTime;
System.out.println("=== ConcurrentHashMap Performance ===");
System.out.println("Duration: " + duration / 1_000_000 + " ms");
System.out.println("Final size: " + map.size());
}
}
Caffeine has established itself as the ideal solution for local caching needs in Java applications through its simple and intuitive API combined with excellent performance.