Caffeine Cache: Introduction, Core Concepts, and Spring Boot Integration

This article provides a comprehensive overview of the Caffeine local cache library, covering its fundamentals, configuration options, loading strategies, eviction policies, statistics, and detailed steps for integrating Caffeine with Spring Boot using annotations and custom cache managers.

IT Xianyu
IT Xianyu
IT Xianyu
Caffeine Cache: Introduction, Core Concepts, and Spring Boot Integration

1. Introduction to Caffeine

Cache is ubiquitous in software, from CPU multi‑level caches to client‑side page caches, serving as a space‑for‑time trade‑off to accelerate data access.

In Java, common cache solutions include EhCache, Memcached, and others, primarily to improve throughput and reduce pressure on persistent storage.

Caffeine is a high‑performance local cache library based on Java 8, derived from Guava, and has become the default cache implementation in Spring 5, offering near‑optimal hit rates and O(1) access time.

2. Caffeine Basics

To use Caffeine, add the following Maven dependency:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <!-- Find the latest version at https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
    <version>3.0.5</version>
</dependency>

2.1 Cache Loading Strategies

Manual Cache : Use put() to store entries and getIfPresent() to retrieve them. To atomically load a missing value, use get(key, k -> value). Remove entries with invalidate(). In multithreaded scenarios, get(key, k -> value) blocks competing threads until the value is loaded, while getIfPresent() returns null immediately if the entry is absent.

Cache<Object, Object> cache = Caffeine.newBuilder()
    .initialCapacity(10)
    .maximumSize(10)
    .expireAfterWrite(1, TimeUnit.SECONDS)
    .expireAfterAccess(1, TimeUnit.SECONDS)
    .removalListener((key, val, removalCause) -> { })
    .recordStats()
    .build();

cache.put("1", "张三");
System.out.println(cache.getIfPresent("1"));
System.out.println(cache.get("2", k -> "默认值"));

LoadingCache (Automatic Loading) : Define a CacheLoader so that get() automatically loads missing values. Bulk loading can be done with getAll() which invokes CacheLoader.loadAll() if implemented.

LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
    .refreshAfterWrite(10, TimeUnit.SECONDS)
    .expireAfterWrite(10, TimeUnit.SECONDS)
    .expireAfterAccess(10, TimeUnit.SECONDS)
    .maximumSize(10)
    .build(key -> new Date().toString());

AsyncCache : Returns a CompletableFuture for asynchronous retrieval. By default it uses ForkJoinPool.commonPool(), but a custom executor can be supplied via Caffeine.executor(Executor). The synchronous() method blocks until the async computation completes and returns a regular cache view.

AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
    .refreshAfterWrite(1, TimeUnit.SECONDS)
    .expireAfterWrite(1, TimeUnit.SECONDS)
    .expireAfterAccess(1, TimeUnit.SECONDS)
    .maximumSize(10)
    .buildAsync(key -> {
        Thread.sleep(1000);
        return new Date().toString();
    });

CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);

2.2 Eviction Policies

Caffeine supports size‑based, weight‑based, time‑based, and reference‑based eviction. Common strategies include LRU (least‑recently‑used), LFU (least‑frequently‑used), and FIFO (first‑in‑first‑out). You can combine multiple policies; an entry is evicted when any configured condition is met.

@Slf4j
public class CacheTest {
    @Test
    public void maximumSizeTest() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
            .maximumSize(10)
            .evictionListener((key, val, removalCause) -> {
                log.info("Evicted cache: key:{} val:{}", key, val);
            })
            .build();
        for (int i = 1; i < 20; i++) {
            cache.put(i, i);
        }
        Thread.sleep(500);
        System.out.println(cache.asMap());
    }
    // Additional tests for weight, expireAfterAccess, expireAfterWrite omitted for brevity
}

2.3 Refresh Mechanism

The refreshAfterWrite() method triggers a refresh after a specified duration, but it only works with LoadingCache or AsyncLoadingCache. The refresh occurs lazily on the next read after the interval.

private static int NUM = 0;

@Test
public void refreshAfterWriteTest() throws InterruptedException {
    LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
        .refreshAfterWrite(1, TimeUnit.SECONDS)
        .build(k -> ++NUM);
    System.out.println(cache.get(1)); // 1
    Thread.sleep(2000);
    System.out.println(cache.getIfPresent(1)); // still 1
    System.out.println(cache.getIfPresent(1)); // now 2 after refresh
}

2.4 Statistics

Enable recordStats() to collect hit/miss counts, load successes/failures, eviction counts, and more.

LoadingCache<String, String> cache = Caffeine.newBuilder()
    .refreshAfterWrite(1, TimeUnit.SECONDS)
    .expireAfterWrite(1, TimeUnit.SECONDS)
    .expireAfterAccess(1, TimeUnit.SECONDS)
    .maximumSize(10)
    .recordStats()
    .build(key -> {
        Thread.sleep(1000);
        return new Date().toString();
    });

cache.put("1", "shawn");
cache.get("1");
System.out.println(cache.stats());

3. Integrating Caffeine with Spring Boot

3.1 Required Annotations

To enable caching, add @EnableCaching to a configuration class and use @Cacheable, @CachePut, @CacheEvict, or @Caching on service methods.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

3.2 Cache Constants

Define a class to hold common cache names and expiration times, optionally loading them from configuration files.

public class CacheConstants {
    public static final int DEFAULT_EXPIRES = 3 * 60;
    public static final int EXPIRES_5_MIN = 5 * 60;
    public static final int EXPIRES_10_MIN = 10 * 60;

    public static final String GET_USER = "GET:USER";
    public static final String GET_DYNAMIC = "GET:DYNAMIC";
}

3.3 Cache Configuration

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = new ArrayList<>();
        for (CacheEnum cacheEnum : CacheEnum.values()) {
            caches.add(new CaffeineCache(cacheEnum.getName(),
                Caffeine.newBuilder()
                    .initialCapacity(50)
                    .maximumSize(1000)
                    .expireAfterAccess(cacheEnum.getExpires(), TimeUnit.SECONDS)
                    .build()));
        }
        cacheManager.setCaches(caches);
        return cacheManager;
    }
}

3.4 Using the Cache

Example of a service method that caches user data. Note that internal method calls bypass the proxy and therefore do not trigger caching.

@Cacheable(value = CacheConstants.GET_USER, key = "'user'+#userId", sync = true)
@CacheEvict
public UserEntity getUserByUserId(Integer userId) {
    UserEntity user = userMapper.findById(userId);
    System.out.println("Queried DB");
    return user;
}

Overall, Caffeine provides a flexible, high‑performance local caching solution that can be seamlessly integrated into Spring Boot applications through standard caching annotations and a customizable cache manager.

JavaCacheCaffeineSpringBoot
IT Xianyu
Written by

IT Xianyu

We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.

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.