Backend Development 30 min read

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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Using Caffeine Cache in Spring Boot: Features, Algorithms, and Configuration

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

YAML:

spring:
  cache:
    type: caffeine
    cache-names:
      - userCache
    caffeine:
      spec: maximumSize=1024,refreshAfterWrite=60s

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

JavaCachingSpring BootCaffeine CacheCache Configuration
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login 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.