A high performance caching library for Java providing Google Guava-inspired API with advanced eviction policies and comprehensive features
—
Caffeine provides asynchronous cache interfaces that return CompletableFuture instances for non-blocking cache operations. The async interfaces enable high-throughput applications to perform cache operations without blocking threads.
The AsyncCache interface provides asynchronous cache operations returning futures.
public interface AsyncCache<K, V> {
// Retrieval operations
CompletableFuture<V> getIfPresent(K key);
CompletableFuture<V> get(K key, Function<? super K, ? extends V> mappingFunction);
CompletableFuture<V> get(K key, BiFunction<? super K, ? super Executor, CompletableFuture<V>> mappingFunction);
CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys, Function<Set<? extends K>, Map<K, V>> mappingFunction);
// Storage operations
void put(K key, CompletableFuture<V> valueFuture);
// Synchronous view
Cache<K, V> synchronous();
}AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
// Simple async retrieval - returns future of value or null
CompletableFuture<String> future1 = asyncCache.getIfPresent("key1");
String value = future1.join(); // null if not present
// Async get with compute function
CompletableFuture<String> future2 = asyncCache.get("key2", k -> {
// This computation runs asynchronously
return "computed_" + k;
});
// Async get with executor-aware compute function
CompletableFuture<String> future3 = asyncCache.get("key3", (key, executor) -> {
return CompletableFuture.supplyAsync(() -> {
// Long-running computation on provided executor
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "async_computed_" + key;
}, executor);
});// Store a completed future
asyncCache.put("immediate", CompletableFuture.completedFuture("immediate_value"));
// Store a future that completes later
CompletableFuture<String> laterFuture = CompletableFuture.supplyAsync(() -> {
// Simulate async computation
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "delayed_value";
});
asyncCache.put("delayed", laterFuture);Set<String> keys = Set.of("key1", "key2", "key3");
// Bulk async retrieval with computation for missing values
CompletableFuture<Map<String, String>> bulkFuture = asyncCache.getAll(keys, missingKeys -> {
Map<String, String> result = new HashMap<>();
for (String key : missingKeys) {
result.put(key, "bulk_" + key);
}
return result;
});
Map<String, String> bulkResult = bulkFuture.join();AsyncCache<String, UserData> userCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
// Chain async operations
CompletableFuture<String> userNameFuture = userCache
.get("user123", userId -> fetchUserFromDatabase(userId))
.thenApply(userData -> userData.getName())
.thenApply(String::toUpperCase);
// Handle results asynchronously
userNameFuture.thenAccept(userName -> {
System.out.println("User name: " + userName);
}).exceptionally(throwable -> {
System.err.println("Failed to get user name: " + throwable.getMessage());
return null;
});CompletableFuture<String> user1Future = asyncCache.get("user1", this::loadUser);
CompletableFuture<String> user2Future = asyncCache.get("user2", this::loadUser);
// Combine results from multiple async cache operations
CompletableFuture<String> combinedFuture = user1Future.thenCombine(user2Future, (user1, user2) -> {
return "Combined: " + user1 + " + " + user2;
});
String combined = combinedFuture.join();The AsyncLoadingCache interface extends AsyncCache and provides automatic async loading.
public interface AsyncLoadingCache<K, V> extends AsyncCache<K, V> {
// Automatic async loading
CompletableFuture<V> get(K key);
CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys);
// Synchronous view
LoadingCache<K, V> synchronous();
}AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync(key -> {
// This CacheLoader runs on the default executor
Thread.sleep(100); // Simulate work
return "loaded_" + key;
});
// Get value - loads asynchronously if not present
CompletableFuture<String> loadedFuture = asyncLoadingCache.get("key1");
String loadedValue = loadedFuture.join();AsyncLoadingCache<String, UserData> userAsyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync(new AsyncCacheLoader<String, UserData>() {
@Override
public CompletableFuture<UserData> asyncLoad(String userId, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
// Async loading logic
return databaseService.fetchUser(userId);
}, executor);
}
@Override
public CompletableFuture<Map<String, UserData>> asyncLoadAll(
Set<? extends String> userIds, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
// Efficient bulk async loading
return databaseService.fetchUsers(userIds);
}, executor);
}
});
// Efficient bulk async loading
CompletableFuture<Map<String, UserData>> usersFuture =
userAsyncCache.getAll(Set.of("user1", "user2", "user3"));AsyncLoadingCache<String, String> refreshingAsyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(Duration.ofMinutes(5))
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> {
// Async refresh computation
return fetchLatestValue(key);
}, executor));
// The cache will automatically refresh values in the background
CompletableFuture<String> valueFuture = refreshingAsyncCache.get("key1");
// Manual async refresh
CompletableFuture<String> refreshFuture = refreshingAsyncCache.synchronous().refresh("key1");Both async cache interfaces provide synchronous views for mixed usage patterns.
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
// Get synchronous view
Cache<String, String> syncView = asyncCache.synchronous();
// Use synchronous operations on the same underlying cache
asyncCache.put("async_key", CompletableFuture.completedFuture("async_value"));
String value = syncView.getIfPresent("async_key"); // "async_value"
syncView.put("sync_key", "sync_value");
CompletableFuture<String> asyncValue = asyncCache.getIfPresent("sync_key");
// Future completed with "sync_value"AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync(key -> "loaded_" + key);
// Get synchronous loading view
LoadingCache<String, String> syncLoadingView = asyncLoadingCache.synchronous();
// Mixed usage
CompletableFuture<String> asyncLoaded = asyncLoadingCache.get("async_key");
String syncLoaded = syncLoadingView.get("sync_key");AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
CompletableFuture<String> failingFuture = asyncCache.get("error_key", key -> {
if (key.equals("error_key")) {
throw new RuntimeException("Computation failed");
}
return "success_" + key;
});
// Handle failures
failingFuture
.thenApply(value -> "Processed: " + value)
.exceptionally(throwable -> {
System.err.println("Computation failed: " + throwable.getMessage());
return "fallback_value";
})
.thenAccept(result -> System.out.println("Final result: " + result));AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync((key, executor) -> {
return CompletableFuture.supplyAsync(() -> {
if (key.startsWith("fail_")) {
throw new RuntimeException("Loading failed for " + key);
}
return "loaded_" + key;
}, executor);
});
// Failed loads result in exceptional completion
CompletableFuture<String> result = asyncLoadingCache.get("fail_test");
result.whenComplete((value, throwable) -> {
if (throwable != null) {
System.err.println("Load failed: " + throwable.getMessage());
// Entry is not cached when loading fails
} else {
System.out.println("Loaded: " + value);
}
});// Configure custom executor for async operations
ForkJoinPool customPool = new ForkJoinPool(20);
AsyncCache<String, String> customExecutorCache = Caffeine.newBuilder()
.maximumSize(1000)
.executor(customPool)
.buildAsync();
// All async computations will use the custom executor
CompletableFuture<String> future = customExecutorCache.get("key", k -> expensiveComputation(k));Async caches store CompletableFuture instances rather than direct values:
// Memory overhead: each entry stores a CompletableFuture wrapper
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
// For completed values, consider using synchronous cache if memory is constrained
Cache<String, String> syncCache = Caffeine.newBuilder()
.maximumSize(1000)
.build();Async caches maintain the same thread-safety guarantees as synchronous caches:
AsyncCache<String, String> threadSafeAsyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.buildAsync();
// Multiple threads can safely access async cache concurrently
ExecutorService executor = Executors.newFixedThreadPool(10);
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 100; i++) {
final int threadId = i;
CompletableFuture<String> future = threadSafeAsyncCache.get("key_" + threadId,
k -> "computed_" + k);
futures.add(future);
}
// Wait for all computations
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();Install with Tessl CLI
npx tessl i tessl/maven-com-github-ben-manes-caffeine--caffeine