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.

Programmer DD
Programmer DD
Programmer DD
Mastering Caffeine Cache: Algorithms, Configurations, and Spring Boot Integration

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=60s

If 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
recordStats

Note: 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.

Caffeine cache architecture diagram
Caffeine cache architecture diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootCaffeine CacheCache EvictionW‑TinyLFU
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.