Using Caffeine Cache in Spring Boot: Features, Algorithms, and Configuration
This article introduces Caffeine Cache, explains its W‑TinyLFU algorithm advantages over traditional FIFO, LRU, and LFU strategies, demonstrates manual, synchronous, and asynchronous loading methods, covers eviction policies, statistics, Spring Boot integration, and provides detailed configuration and code examples for Java developers.
Hello everyone, I am Chen~
Previously we discussed Guava Cache, which wraps get/put operations, provides thread‑safe caching, expiration and eviction policies, and monitoring. When the cache exceeds its maximum size it uses LRU for replacement. This article introduces a newer local cache framework: Caffeine Cache, which builds on Guava's ideas and improves the underlying algorithms.
The blog mainly explains how to use Caffeine Cache and how to integrate it with Spring Boot.
Caffeine Cache Algorithm Advantages – W‑TinyLFU
What does Caffeine actually optimise? Besides LRU, common eviction algorithms also include FIFO and LFU:
FIFO : First‑in‑first‑out; early entries are evicted first, leading to low hit rates.
LRU : Least‑recently‑used; each access moves the entry to the front. However, a hot key accessed many times in one minute may be evicted if it is not accessed later while other keys receive traffic.
LFU : Least‑frequently‑used; records access frequency and evicts the least frequent. This avoids the time‑window problem of LRU.
Each strategy has trade‑offs in implementation cost and hit rate. Guava Cache essentially wraps LRU; a more advanced algorithm with similar features would outperform it.
Limitations of LFU : LFU works well only when the access‑frequency distribution remains stable. If a newly popular item (e.g., a hot TV series) becomes stale after a month, its high frequency still prevents eviction, harming other items.
Advantages and Limitations of LRU : LRU handles sudden traffic spikes because it does not rely on accumulated frequency, but it predicts future accesses solely from recent history, which can be inaccurate.
These limitations reduce cache hit rates. The modern cache design described by a former Google engineer, W‑TinyLFU, combines the strengths of LFU and LRU. Caffeine adopts the Window TinyLFU eviction policy, achieving near‑optimal hit rates.
When the access pattern does not change over time, LFU yields the best hit rate, but it has two drawbacks: Maintaining frequency counters for every entry incurs high overhead. If the access pattern changes, stale frequency data prevents newly hot items from being cached. Most caches therefore rely on LRU or its variants. LRU avoids the cost of frequency metadata but may need more space to match LFU's hit rate. A modern cache should combine both strengths.
TinyLFU keeps recent access frequencies as a filter; only entries that satisfy TinyLFU are admitted. It solves two challenges:
Avoiding the high cost of maintaining frequency information.
Adapting to time‑varying access patterns.
To address the first challenge, TinyLFU uses a Count‑Min Sketch, which stores frequency data in a compact space with a low false‑positive rate. For the second challenge, TinyLFU applies a sliding‑window time‑decay: each time a record is added to the sketch, a counter increments; when the counter reaches a size W , all sketch values are halved, providing decay.
W‑TinyLFU is designed for sparse, bursty accesses. In scenarios with few items but huge burst traffic, plain TinyLFU cannot retain those items because they never accumulate enough frequency. W‑TinyLFU combines LFU for the majority of traffic with LRU for sudden spikes.
The sketching technique can be visualised as follows:
Usage
Caffeine Cache GitHub repository:
https://github.com/ben-manes/caffeine
Current latest version (Maven dependency):
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>Cache Loading Strategies
Manual Loading
Provide a synchronous function when calling cache.get(key, fn) . If the key is absent, the function generates the value.
/**
* Manual loading
*/
public Object manualOperator(String key) {
Cache
cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
.build();
// If key does not exist, generate value via the supplied function
Object value = cache.get(key, t -> setValue(key).apply(key));
cache.put("hello", value);
// Retrieve if present, otherwise null
Object ifPresent = cache.getIfPresent(key);
// Remove a key
cache.invalidate(key);
return value;
}
public Function
setValue(String key) {
return t -> key + "value";
}Synchronous Loading
When building the cache, pass a CacheLoader implementation that defines load(key) .
/**
* Synchronous loading
*/
public Object syncOperator(String key) {
LoadingCache
cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> setValue(key).apply(key));
return cache.get(key);
}
public Function
setValue(String key) {
return t -> key + "value";
}Asynchronous Loading
AsyncLoadingCache extends LoadingCache and loads values via an Executor , returning a CompletableFuture . It follows a reactive model.
/**
* Asynchronous loading
*/
public Object asyncOperator(String key) {
AsyncLoadingCache
cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> setAsyncValue(key).get());
return cache.get(key);
}
public CompletableFuture
setAsyncValue(String key) {
return CompletableFuture.supplyAsync(() -> key + "value");
}Eviction Policies
Caffeine provides three eviction strategies: size‑based, time‑based, and reference‑based.
Size‑Based Eviction
Two ways: by entry count ( maximumSize ) or by weight ( maximumWeight with a custom weigher).
// Evict based on entry count
LoadingCache
cache = Caffeine.newBuilder()
.maximumSize(10000)
.build(key -> function(key));
// Evict based on weight (weight is used only to determine size, not eviction decision)
LoadingCache
cache1 = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher((key, value) -> function1(key))
.build(key -> function(key));Time‑Based Eviction
// Fixed expiration after access
LoadingCache
cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> function(key));
// Fixed expiration after write
LoadingCache
cache1 = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> function(key));
// Custom expiry implementation
LoadingCache
cache2 = Caffeine.newBuilder()
.expireAfter(new Expiry
() {
@Override
public long expireAfterCreate(String key, Object value, long currentTime) {
return TimeUnit.SECONDS.toNanos(seconds);
}
@Override
public long expireAfterUpdate(String s, Object o, long l, long l1) {
return 0;
}
@Override
public long expireAfterRead(String s, Object o, long l, long l1) {
return 0;
}
})
.build(key -> function(key));Caffeine offers three time‑based strategies: expireAfterAccess , expireAfterWrite , and a custom Expiry . Deletion is performed lazily or via a scheduled task, both O(1).
Reference‑Based Eviction
Java provides four reference types:
Reference Type
GC Timing
Purpose
Lifetime
Strong Reference
Never
General object usage
Ends when JVM stops
Soft Reference
When memory is low
Object caching
Cleared on memory pressure
Weak Reference
During GC
Object caching
Cleared after GC run
Phantom Reference
Never
Track object finalisation
Ends when JVM stops
// Evict when both key and value have no strong references
LoadingCache
cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> function(key));
// Evict when JVM needs memory (soft values)
LoadingCache
cache1 = Caffeine.newBuilder()
.softValues()
.build(key -> function(key));Note: AsyncLoadingCache does not support weak or soft values.
Caffeine.weakKeys() stores keys with weak references; if no other strong reference exists, the entry can be reclaimed, and key equality falls back to identity (==) rather than equals() . Similarly, weakValues() and softValues() affect value handling.
Removal Listener
Cache
cache = Caffeine.newBuilder()
.removalListener((String key, Object value, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();Write‑Through to External Store
LoadingCache
cache2 = Caffeine.newBuilder()
.writer(new CacheWriter
() {
@Override
public void write(String key, Object value) {
// Write to external storage
}
@Override
public void delete(String key, Object value, RemovalCause cause) {
// Delete from external storage
}
})
.build(key -> function(key));This is useful for multi‑level cache architectures.
Statistics
Cache
cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
// CacheStats provides hitRate(), evictionCount(), averageLoadPenalty(), etc.Caffeine Cache as the Default Cache in Spring Boot
Spring Boot 1.x used Guava Cache by default. Starting with Spring Boot 2.x (Spring 5), Caffeine replaces Guava because of its superior eviction algorithm.
Adding 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 Annotation
@SpringBootApplication
@EnableCaching
public class SingleDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(SingleDatabaseApplication.class, args);
}
}Configuration via Properties or YAML
Properties:
spring.cache.cache-names=cache1
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10sYAML:
spring:
cache:
type: caffeine
cache-names:
- userCache
caffeine:
spec: maximumSize=1024,refreshAfterWrite=60sIf refreshAfterWrite is used, a CacheLoader bean must be defined.
import com.github.benmanes.caffeine.cache.CacheLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
@Bean
public CacheLoader
cacheLoader() {
return new CacheLoader
() {
@Override
public Object load(String key) throws Exception {
return null; // load from DB
}
@Override
public Object reload(String key, Object oldValue) throws Exception {
return oldValue; // refresh logic
}
};
}
}Common Caffeine Configuration Options
initialCapacity=[int] // initial cache size
maximumSize=[long] // max number of entries
maximumWeight=[long] // max weight (requires weigher)
expireAfterAccess=[duration] // expire after last read/write
expireAfterWrite=[duration] // expire after write
refreshAfterWrite=[duration] // refresh after write (requires CacheLoader)
weakKeys // use weak references for keys
weakValues // use weak references for values
softValues // use soft references for values
recordStats // enable statistics collection
// Rules:
// - expireAfterWrite overrides expireAfterAccess if both are set.
// - maximumSize and maximumWeight cannot be used together.
// - weakValues and softValues cannot be used together.Bean‑Based Cache Manager Example
package com.rickiyang.learn.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List
caches = new ArrayList<>();
// Example cache beans
caches.add(new CaffeineCache("userCache", Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(10000)
.build()));
caches.add(new CaffeineCache("deptCache", Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(10000)
.build()));
cacheManager.setCaches(caches);
return cacheManager;
}
}Annotation‑Based Cache Operations
Spring provides @Cacheable , @CachePut , @CacheEvict , @Caching , and @CacheConfig to simplify cache usage.
package com.rickiyang.learn.cache;
import com.rickiyang.learn.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserCacheService {
@Cacheable(value = "userCache", key = "#id", sync = true)
public void getUser(long id) {
// fetch from DB if not cached
}
@CachePut(value = "userCache", key = "#user.id")
public void saveUser(User user) {
// persist to DB and update cache
}
@CacheEvict(value = "userCache", key = "#user.id")
public void delUser(User user) {
// delete from DB and evict cache
}
}SpEL Context Variables for Cache Keys
Name
Location
Description
Example
methodName
root
Current method name
#root.methodName
method
root
Current method object
#root.method.name
target
root
Target object instance
#root.target
targetClass
root
Target class
#root.targetClass
args
root
Method arguments array
#root.args[0]
caches
root
List of caches used
#root.caches[0].name
Argument Name
execution context
Method parameter (e.g., #user.id)
#user.id
result
execution context
Method return value (used in
unless)
#result
SpEL Operators
Category
Operators
Relational
<, >, <=, >=, ==, !=, lt, gt, le, ge, eq, ne
Arithmetic
+, -, *, /, %, ^
Logical
&&, ||, !, and, or, not, between, instanceof
Conditional
?: (ternary), ?: (elvis)
Regex
matches
Other
?. , ?[…] , ![…] , ^[…] , $[…]
Backend Technical Community Group
Build a high‑quality technical exchange community. Developers, recruiters, and anyone interested in sharing job referrals are welcome!
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.