Caffeine

Java cachelocal cachehigh performanceSpring BootWindow TinyLFU

GitHub Overview

ben-manes/caffeine

A high performance caching library for Java

Stars17,137
Watchers363
Forks1,666
Created:December 13, 2014
Language:Java
License:Apache License 2.0

Topics

None

Star History

ben-manes/caffeine Star History
Data as of: 10/22/2025, 09:55 AM

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);
    }
}