Caffeine

High-performance in-application caching library for Java. Successor to Guava Cache with optimized algorithms and rich features.

cache-libraryJavaSpring-Bootin-application-cachehigh-performanceGuava-successor

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.