Mastering Caffeine Cache in Spring Boot 3.2: Performance, Eviction & Async Usage
This article introduces the high‑performance Caffeine caching library for Spring Boot 3.2, explains its core features, demonstrates detailed performance benchmarks, and provides practical code examples covering synchronous and asynchronous operations, eviction policies, removal listeners, refresh mechanisms, and statistics collection.
1. Introduction
Caffeine is a high‑performance Java 8‑based caching library that offers near‑optimal hit rates. Unlike a plain ConcurrentMap, a Caffeine Cache can be configured to evict entries automatically, and its LoadingCache and AsyncLoadingCache provide convenient automatic loading.
Features
Automatic loading of elements, with optional asynchronous loading.
Capacity‑based eviction using near‑frequency algorithms.
Time‑based eviction based on last access or write.
Asynchronous refresh on stale reads.
Keys are wrapped with weak references.
Values can be wrapped with weak or soft references.
Eviction or removal notifications.
Write‑through to an external data source.
Cache access statistics collection.
2. Performance Test
Benchmarks were run with Java Microbenchmark Harness (JMH). The “generation” test measures the cost of locking when creating cache entries. The “read” test uses 8 threads reading from a cache with a maximum size. The “write” test uses 8 threads performing concurrent writes.
Read test (75% reads / 25% writes) uses 6 reader threads and 2 writer threads.
Write test (100% writes) uses 8 writer threads.
3. Practical Examples
3.1 Add Dependency
<code><dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</code>3.2 Adding Elements
Synchronous operations
<code>static final Cache<Long, User> cache = Caffeine.newBuilder()
// expire after 10 minutes regardless of usage
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
public static void basicCache() {
User user = cache.getIfPresent(1L);
user = cache.get(1L, key -> new User(key, "Zhang San", 22));
cache.put(1L, user);
cache.invalidate(1L);
}
</code>Asynchronous operations
<code>static AsyncCache<Long, User> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
public static void asyncCache() {
CompletableFuture<User> cf = cache.getIfPresent(1L);
cf.thenAccept(System.out::println);
cf = cache.get(1L, key -> new User(key, "Zhang San", 22));
cache.put(1L, CompletableFuture.supplyAsync(() -> new User(1L, "Li Si", 33)));
cache.synchronous().invalidate(1L);
}
</code>3.3 Eviction Strategies
Caffeine provides three eviction policies: capacity‑based, time‑based, and reference‑based.
Capacity‑based – evicts when the maximum size is exceeded, using near‑frequency algorithms.
<code>public static User createUser(Long key) {
return new User(key, "name - " + key, new Random().nextInt(100));
}
public static void main(String[] args) {
LoadingCache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(3)
.build(key -> createUser(key));
System.out.println(cache.get(1L));
System.out.println(cache.get(2L));
System.out.println(cache.get(3L));
System.out.println(cache.get(4L)); // evicts key 2
}
</code>Result shows key 2 being evicted when key 4 is inserted.
Time‑based – expireAfterAccess, expireAfterWrite, or custom Expiry.
<code>Cache<Long, User> cache = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
cache.put(1L, new User(1L, "Admin", 20));
System.out.println(cache.getIfPresent(1L));
TimeUnit.SECONDS.sleep(3);
System.out.println(cache.getIfPresent(1L)); // null after 3 s
</code>Reference‑based – keys are weakly referenced, values can be weak or soft referenced (not detailed here).
3.4 Removal
<code>static final Cache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(10)
.build();
public static void remove(Long key) {
cache.invalidate(key);
cache.invalidateAll(Arrays.asList(key));
cache.invalidateAll();
}
</code>Listeners can be attached to receive eviction or removal callbacks.
<code>static Cache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(3)
.evictionListener((k, v, cause) -> System.out.printf("Key %s was evicted (%s)%n", k, cause))
.removalListener((k, v, cause) -> System.out.printf("Key %s was removed (%s)%n", k, cause))
.build();
</code>3.5 Refresh
<code>static final Cache<Long, User> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(3, TimeUnit.SECONDS)
.maximumSize(10)
.build(key -> createUser(key));
private static User createUser(Long key) {
int r = new Random().nextInt(100);
return new User(key, "China - " + r, r);
}
</code>Refresh occurs only when the key is accessed after the refresh interval.
3.6 Statistics
<code>static final Cache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(10)
.recordStats()
.build();
CacheStats stats = cache.stats();
System.out.println("hitRate: " + stats.hitRate());
System.out.println("evictionCount: " + stats.evictionCount());
System.out.println("averageLoadPenalty: " + stats.averageLoadPenalty());
</code>4. Comprehensive Example
For a full‑stack demonstration of Caffeine in a real Spring Boot project, refer to the linked article “Annotation + Caffeine: High‑Performance API Rate Limiting”.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.