Mastering Caffeine Cache: Algorithms, Configurations, and Spring Boot Integration
This article explains the advantages of Caffeine Cache's W‑TinyLFU algorithm over traditional FIFO, LRU, and LFU policies, details its eviction and reference‑type strategies, and provides step‑by‑step guidance for integrating Caffeine into Spring Boot applications with code examples and configuration tips.
Earlier we discussed Guava Cache, which wraps get/put operations, provides thread‑safe access, expiration, eviction, and monitoring, using LRU when the cache exceeds its maximum size. This article introduces Caffeine Cache, a modern local cache built on Guava's ideas but optimized with a superior algorithm.
1. Algorithmic Advantages – W‑TinyLFU
Caffeine improves on traditional eviction policies such as FIFO, LRU, and LFU. FIFO evicts the oldest entries, leading to low hit rates. LRU evicts the least‑recently used entry, but can mistakenly evict hot data that spikes briefly. LFU tracks access frequency to evict the least‑frequent items, yet struggles when access patterns change over time.
Both FIFO, LRU, and LFU have trade‑offs; Guava essentially wraps LRU. Caffeine adopts the W‑TinyLFU algorithm, which combines LFU and LRU to achieve near‑optimal hit rates while handling both steady and bursty workloads.
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 changing patterns.
TinyLFU uses a Count‑Min Sketch to store recent access frequencies with low memory overhead and a false‑positive rate. To handle temporal changes, it applies a sliding‑window decay: each time the sketch reaches a size W, all counters are halved, providing automatic decay.
W‑TinyLFU adds a windowed LRU layer to protect sparse, bursty items that would otherwise be evicted by pure LFU.
2. Using Caffeine
GitHub repository:
https://github.com/ben-manes/caffeine
Current Maven dependency (version 2.6.2):
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>2.1 Cache Loading Strategies
Manual Loading
Provide a synchronous function to compute a value when a key is missing.
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 t -> key + "value";
}Synchronous Loading
Supply a CacheLoader implementation when building the cache.
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 t -> key + "value";
}Asynchronous Loading
Use AsyncLoadingCache with an AsyncCacheLoader that returns a CompletableFuture.
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");
}2.2 Eviction Policies
Caffeine offers three eviction strategies:
Size‑based eviction (by entry count or weight).
Time‑based eviction ( expireAfterAccess, expireAfterWrite, or custom Expiry).
Reference‑based eviction (weak or soft references).
Size‑based
// Evict by entry count
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.build(k -> function(k));
// Evict by weight
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher((k, v) -> computeWeight(k))
.build(k -> function(k));Note: maximumSize and maximumWeight cannot be used together.
Time‑based
// Fixed expiration after access
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(k -> function(k));
// Fixed expiration after write
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(k -> function(k));
// Custom expiry implementation
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, Object>() {
@Override public long expireAfterCreate(String k, Object v, long currentTime) {
return TimeUnit.SECONDS.toNanos(seconds);
}
@Override public long expireAfterUpdate(String k, Object v, long currentTime, long currentDuration) { return 0; }
@Override public long expireAfterRead(String k, Object v, long currentTime, long currentDuration) { return 0; }
})
.build(k -> function(k));Caffeine provides three built‑in time‑based policies: expireAfterAccess, expireAfterWrite, and custom Expiry. All have O(1) complexity.
Reference‑based
Java offers four reference types:
Strong – never reclaimed until JVM exits.
Soft – reclaimed when memory is low (useful for cache values).
Weak – reclaimed on the next GC cycle if no strong references exist.
Phantom – used for post‑GC cleanup notifications.
// Weak keys and values
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(k -> function(k));
// Soft values only
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.softValues()
.build(k -> function(k));Note: AsyncLoadingCache does not support weak or soft references, and weakValues cannot be combined with softValues.
3. Removal Listener and Cache Writer
Cache<String, Object> cache = Caffeine.newBuilder()
.removalListener((k, v, cause) ->
System.out.printf("Key %s was removed (%s)%n", k, cause))
.build();CacheWriter allows persisting cache entries to external storage or cleaning them up when evicted.
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));4. Statistics
Enable statistics with recordStats() and retrieve them via Cache.stats().
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
CacheStats stats = cache.stats();
// stats.hitRate(), stats.evictionCount(), stats.averageLoadPenalty(), ...5. Spring Boot Integration
Spring Boot 2.x replaces Guava with Caffeine as the default local cache. To use Caffeine:
5.1 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>5.2 Enable Caching
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}5.3 Configuration via Properties or YAML
# application.properties
spring.cache.cache-names=cache1
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10s # application.yml
spring:
cache:
type: caffeine
cache-names:
- userCache
caffeine:
spec: maximumSize=1024,refreshAfterWrite=60sIf refreshAfterWrite is used, a CacheLoader<Object, Object> bean must be defined.
@Configuration
public class CacheConfig {
@Bean
public CacheLoader<String, Object> cacheLoader() {
return new CacheLoader<String, Object>() {
@Override public Object load(String key) { return null; }
@Override public Object reload(String key, Object oldValue) { return oldValue; }
};
}
}5.4 Common Caffeine Settings
initialCapacity=[int]
maximumSize=[long]
maximumWeight=[long]
expireAfterAccess=[duration]
expireAfterWrite=[duration]
refreshAfterWrite=[duration]
weakKeys
weakValues
softValues
recordStatsNote: expireAfterWrite takes precedence over expireAfterAccess when both are present; maximumSize and maximumWeight are mutually exclusive; weakValues and softValues cannot be used together.
5.5 Bean‑Based Cache Creation
@Configuration
public class CacheConfig {
@Bean @Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
caches.add(new CaffeineCache("userCache",
Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(10000)
.build()));
// add more caches as needed
manager.setCaches(caches);
return manager;
}
}5.6 Annotation‑Based Cache Operations
Use Spring's cache annotations to interact with Caffeine:
@Service
public class UserCacheService {
@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 DB */ return user; }
@CacheEvict(value = "userCache", key = "#user.id")
public void deleteUser(User user) { /* delete from DB */ }
}Key expressions can use SpEL variables such as #root.methodName, #args[0], or directly #id. Additional cache annotations include @CacheConfig, @Caching, and @CacheEvict with various options.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
