Mastering Caffeine Cache in Spring Boot: W‑TinyLFU, Configurations, and Code Samples
This article introduces Caffeine Cache as a high‑performance replacement for Guava Cache, explains its W‑TinyLFU eviction algorithm, demonstrates various loading and eviction strategies with code examples, and shows how to integrate and configure it in Spring Boot applications.
Caffeine Cache is a modern Java local‑cache library that builds on Guava Cache’s API while offering thread‑safe operations, expiration policies, eviction strategies, and monitoring. Its key advantage is the use of the W‑TinyLFU algorithm, which provides near‑optimal hit rates.
1. Algorithmic Advantages – W‑TinyLFU
Traditional eviction policies such as FIFO, LRU, and LFU each have drawbacks. FIFO evicts the oldest entry, leading to low hit rates. LRU removes the least‑recently‑used entry but can discard hot items after a short burst. LFU tracks access frequency but struggles when access patterns change over time.
W‑TinyLFU combines the strengths of LFU and LRU: it uses a sliding‑window Count‑Min Sketch to record recent frequencies (TinyLFU) and a windowed LRU to protect newly accessed items, achieving high hit rates with low overhead.
LFU works best when access patterns are stable; otherwise it incurs high maintenance cost and may retain stale hot items. LRU adapts quickly to bursts but cannot guarantee the same hit rate as LFU under steady workloads.
TinyLFU maintains recent frequency information using a Count‑Min Sketch, which stores frequencies in a compact data structure with a low false‑positive rate. To handle time‑varying patterns, it applies a simple reset: when the sketch’s counter reaches a predefined window size
W, all counters are halved, effectively decaying older frequencies.
2. Usage
Add the Maven dependency:
<code><dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency></code>2.1 Cache Loading Strategies
Manual loading – provide a function to compute a value when a key is missing.
<code>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 ifPresent = cache.getIfPresent(key);
cache.invalidate(key);
return value;
}
public Function<String, Object> setValue(String key) {
return t -> key + "value";
}</code>Synchronous loading – supply a
CacheLoaderwhen building the cache.
<code>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);
}</code>Asynchronous loading – use
AsyncLoadingCachewith an
Executorand return a
CompletableFuture.
<code>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");
}</code>2.2 Eviction Policies
Size‑based eviction
<code>// By entry count
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.build(k -> function(k));
// By weight (custom weight function)
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher((k, v) -> computeWeight(k, v))
.build(k -> function(k));
</code>Note: maximumSize and maximumWeight cannot be used together.
Time‑based eviction
<code>// Expire after a period of inactivity
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(k -> function(k));
// Expire a fixed time after write
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(k -> function(k));
</code>Caffeine also supports a custom Expiry implementation for fine‑grained control.
Reference‑based eviction
<code>// Weak keys and values – removed when GC discards them
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(k -> function(k));
// Soft values – kept until memory pressure forces eviction
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.softValues()
.build(k -> function(k));
</code>AsyncLoadingCache does not support weak or soft references, and weakValues cannot be combined with softValues .
2.3 Removal Listener
<code>Cache<String, Object> cache = Caffeine.newBuilder()
.removalListener((key, value, cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();</code>2.4 Writing to External Storage
<code>LoadingCache<String, Object> cache = Caffeine.newBuilder()
.writer(new CacheWriter<String, Object>() {
@Override
public void write(String key, Object value) {
// write to external store
}
@Override
public void delete(String key, Object value, RemovalCause cause) {
// delete from external store
}
})
.build(k -> function(k));</code>2.5 Statistics
<code>Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
CacheStats stats = cache.stats();
// stats.hitRate(), stats.evictionCount(), stats.averageLoadPenalty()
</code>3. Caffeine Cache in Spring Boot
Spring Boot 2.x replaces the default Guava Cache with Caffeine Cache.
3.1 Maven Dependencies
<code><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></code>3.2 Enable Caching
<code>@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}</code>3.3 Configuration via Properties/YAML
Properties:
<code>spring.cache.cache-names=cache1
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10s</code>YAML:
<code>spring:
cache:
type: caffeine
cache-names:
- userCache
caffeine:
spec: maximumSize=1024,refreshAfterWrite=60s</code>If
refreshAfterWriteis used, a
CacheLoaderbean must be defined:
<code>@Configuration
public class CacheConfig {
@Bean
public CacheLoader<String, Object> cacheLoader() {
return new CacheLoader<String, Object>() {
@Override
public Object load(String key) {
return null; // load from DB
}
@Override
public Object reload(String key, Object oldValue) {
return oldValue; // refresh logic
}
};
}
}</code>3.4 Cache Manager Bean (Bean‑based Configuration)
<code>@Configuration
public class CacheConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
// Example: userCache with 60‑second TTL and max 10,000 entries
caches.add(new CaffeineCache("userCache",
Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(10_000)
.recordStats()
.build()));
manager.setCaches(caches);
return manager;
}
}</code>3.5 Annotation‑Based Cache Operations
@Cacheable– caches the method result; skips execution if a cached value exists.
@CachePut– always executes the method and updates the cache.
@CacheEvict– removes entries from the cache.
@Caching– groups multiple cache annotations on a single method.
@CacheConfig– defines common cache settings at the class level.
Example service using these annotations:
<code>@Service
public class UserCacheService {
@Cacheable(value = "userCache", key = "#id", sync = true)
public User getUser(long id) {
// fetch from DB
return null;
}
@CachePut(value = "userCache", key = "#user.id")
public User saveUser(User user) {
// persist to DB
return user;
}
@CacheEvict(value = "userCache", key = "#user.id")
public void deleteUser(User user) {
// delete from DB
}
}</code>3.6 SpEL Context Variables for Keys
#root.methodName– current method name.
#root.method.name– full method signature.
#root.target– target object instance.
#root.args[0]– first argument.
#result– method return value (available for
unlesscondition).
Parameter names can be referenced directly, e.g.,
#idor
#p0.
These expressions allow fine‑grained control over cache keys and conditional caching.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.