Mastering Caffeine Cache in Spring Boot: Algorithms, Configurations, and Best Practices
This article explains how Caffeine Cache improves on Guava Cache with the modern W‑TinyLFU algorithm, details its eviction strategies, shows how to configure it in Spring Boot, and provides code examples for manual, synchronous, and asynchronous loading, as well as annotation‑driven usage.
Caffeine Cache Overview
While Guava Cache offers basic get/put operations, thread‑safety, expiration, and eviction, Caffeine Cache builds on Guava’s ideas and introduces a more efficient eviction algorithm called W‑TinyLFU.
Algorithmic Advantages
Caffeine replaces simple LRU with a hybrid approach that combines FIFO, LRU, and LFU, addressing each algorithm’s drawbacks. LFU provides high hit rates when access patterns are stable, but struggles with changing workloads. LRU handles bursts well but may evict hot items prematurely. W‑TinyLFU merges LFU’s long‑term frequency tracking with LRU’s short‑term recency handling.
W‑TinyLFU uses a Count‑Min Sketch to record recent access frequencies in a compact data structure, applying a sliding‑window decay to adapt to changing patterns. This design efficiently filters which entries are eligible for insertion, achieving near‑optimal hit rates.
When access patterns are stable, LFU yields the best hit rate, but it incurs high overhead for maintaining frequency counters and cannot adapt quickly to shifts in popularity.
Cache Filling Strategies
Manual Loading
public Object manualOperator(String key) {
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
.build();
Object value = cache.get(key, k -> setValue(k).apply(k));
cache.put("hello", value);
Object present = cache.getIfPresent(key);
cache.invalidate(key);
return value;
}
public Function<String, Object> setValue(String key) {
return k -> key + "value";
}Synchronous Loading
public Object syncOperator(String key) {
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> setValue(k).apply(k));
return cache.get(key);
}
public Function<String, Object> setValue(String key) {
return k -> key + "value";
}Asynchronous Loading
public Object asyncOperator(String key) {
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> setAsyncValue(k).get());
return cache.get(key);
}
public CompletableFuture<Object> setAsyncValue(String key) {
return CompletableFuture.supplyAsync(() -> key + "value");
}Eviction Policies
Caffeine supports three main eviction strategies:
Size‑based eviction (maximumSize or maximumWeight)
Time‑based eviction (expireAfterAccess, expireAfterWrite, custom Expiry)
Reference‑based eviction (weakKeys, weakValues, softValues)
Size‑based eviction can be configured by entry count or weight; time‑based eviction removes entries after a fixed period of inactivity or write; reference‑based eviction lets the garbage collector reclaim entries when only weak/soft references remain.
Integration with Spring Boot
Spring Boot 2.x replaces Guava Cache with Caffeine as the default local cache. To use Caffeine:
Add dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>Enable caching with @EnableCaching on the main application class.
Configure cache specifications via application.properties or application.yml, e.g.:
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10sOr define beans for fine‑grained control, creating CaffeineCache instances with custom TTL, maximum size, and statistics recording.
Annotation‑Driven Cache Operations
Spring’s cache annotations work with Caffeine:
@Cacheable(value = "userCache", key = "#id", sync = true)
public User getUser(Long id) { /* query DB */ }
@CachePut(value = "userCache", key = "#user.id")
public User saveUser(User user) { /* persist */ }
@CacheEvict(value = "userCache", key = "#user.id")
public void deleteUser(User user) { /* remove */ }These annotations automatically handle cache reads, writes, and evictions based on method execution.
Cache Writer and Removal Listener
Cache<String, Object> cache = Caffeine.newBuilder()
.removalListener((k, v, cause) ->
System.out.printf("Key %s was removed (%s)%n", k, cause))
.writer(new CacheWriter<String, Object>() {
public void write(String key, Object value) { /* write to external store */ }
public void delete(String key, Object value, RemovalCause cause) { /* delete from store */ }
})
.build();Statistics
Enable statistics with .recordStats() and retrieve metrics such as hit rate, eviction count, and average load penalty via cache.stats().
Reference Types in Java
Caffeine supports weak and soft references for keys and values, allowing the garbage collector to reclaim entries when memory is low. Weak keys/values use identity comparison; soft values are reclaimed only under memory pressure.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
