Mastering Caffeine: High‑Performance Local Cache for Java Applications

This article provides a comprehensive guide to Caffeine, a high‑performance Java local cache library, covering its fundamentals, loading strategies, eviction policies, refresh mechanisms, statistics, and step‑by‑step integration with Spring Boot using annotations such as @Cacheable and configuration examples.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Mastering Caffeine: High‑Performance Local Cache for Java Applications

1. Introduction to Caffeine

1.1 Cache Overview

Cache is ubiquitous in software, from CPU caches to client‑side page caches. It trades space for time, arranging data so that subsequent accesses are faster.

In Java, common cache solutions include EhCache, Memcached, and others, which aim to improve throughput and reduce pressure on the persistence layer.

Cache types can be divided into local and distributed; Caffeine is an excellent local cache, while Redis is often used for distributed caching.

1.2 What is Caffeine?

https://github.com/ben-manes/caffeine

Caffeine is a high‑performance local cache library for Java 8, derived from Guava and adopted as the default cache implementation in Spring 5. It offers O(1) access and automatic eviction of rarely used entries, unlike ConcurrentMap which retains all entries until explicitly removed.

ConcurrentMap stores all entries until you explicitly remove them.

Caffeine automatically evicts "unused" data based on the configured policy.

Thus, a cache can be viewed as a Map with storage and eviction strategies.

Caffeine diagram
Caffeine diagram

2. Caffeine Basics

To use Caffeine, add the following Maven dependency:

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

2.1 Cache Loading Strategies

2.1.1 Manual Cache

The simplest cache requires manual operations. Use put() to add entries, get(key, k->value) for atomic loading when a key is absent, and invalidate() to remove entries.

In multithreaded scenarios, get(key, k->value) blocks competing threads until the cache is updated, while getIfPresent() returns null immediately without blocking.

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", o -> "默认值"));

2.1.2 LoadingCache (automatic)

LoadingCache automatically loads missing or expired entries via CacheLoader.load(). getAll() iterates all keys and calls get() unless CacheLoader.loadAll() is 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());

2.1.3 AsyncCache

AsyncCache returns CompletableFuture, adapting the cache to asynchronous programming. By default it uses ForkJoinPool.commonPool(), but a custom executor can be supplied via Caffeine.executor(Executor). The synchronous() method provides a blocking 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

Eviction can be based on capacity, weight, or time. Caffeine supports four eviction settings: size (LFU), weight, time, and reference (rarely used).

LRU – least recently used.

LFU – least frequently used.

FIFO – first in, first out.

2.3 Refresh Mechanism

refreshAfterWrite()

triggers automatic refresh after a specified interval and works only with LoadingCache and AsyncLoadingCache.

private static int NUM = 0;

@Test
public void refreshAfterWriteTest() throws InterruptedException {
    LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
        .refreshAfterWrite(1, TimeUnit.SECONDS)
        .build(integer -> ++NUM);

    System.out.println(cache.get(1)); // 1
    Thread.sleep(2000);
    System.out.println(cache.getIfPresent(1)); // 1 (still old)
    System.out.println(cache.getIfPresent(1)); // 2 (refreshed)
}

2.4 Statistics

Enabling recordStats() collects hit/miss counts, hit rate, load times, eviction counts, etc.

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());

2.5 Summary

Cache configurations can be freely combined. Two common patterns are:

Set maxSize and refreshAfterWrite, omit expireAfterWrite/expireAfterAccess. This offers better performance but may return stale data when expireAfterWrite is used.

Set maxSize and expireAfterWrite/expireAfterAccess without refreshAfterWrite. This ensures data consistency at the cost of performance.

3. Integrating Caffeine with Spring Boot

3.1 @Cacheable and Related Annotations

To use @Cacheable, add spring-boot-starter-cache and enable caching with @EnableCaching.

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

Common Annotations

@Cacheable : caches the method result.

@CachePut : updates the cache after method execution.

@CacheEvict : removes cache entries.

@Caching : combines multiple cache annotations.

Key Attributes

cacheNames/value : name of the cache.

key : cache key, defaults to method parameters or can be a SpEL expression.

keyGenerator : alternative to key.

cacheManager : specifies which cache manager to use.

condition : pre‑execution condition for caching.

unless : post‑execution condition to skip caching.

sync : whether to use synchronous loading.

3.2 Configuration Example

Define a CacheConstants class for common keys and expiration times, then a CacheConfig class that creates a SimpleCacheManager with CaffeineCache instances.

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

3.3 Using the Cache

Cache methods behave like proxies; internal calls bypass caching.

@Cacheable(value = CacheConstants.GET_USER, key = "'user'+#userId", sync = true)
@CacheEvict
public UserEntity getUserByUserId(Integer userId) {
    UserEntity userEntity = userMapper.findById(userId);
    System.out.println("Queried database");
    return userEntity;
}
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.

JavaCacheCaffeineSpringBoot
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.