Backend Development 16 min read

Mastering Caffeine Cache: High‑Performance Java Caching Techniques

This guide introduces Caffeine, a high‑performance Java caching library, explains its core features, configuration options, loading strategies—including manual, automatic, asynchronous, and async loading caches—eviction policies, removal listeners, and statistics collection, providing code examples for each use case.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
Mastering Caffeine Cache: High‑Performance Java Caching Techniques

1. Understanding Caffeine

Caffeine is a high‑performance caching library built on Java 8 that offers near‑optimal hit rates and is the default local cache implementation in Spring Boot.

It provides a flexible builder to create caches with features such as automatic loading (optionally asynchronous), size‑based eviction, time‑based expiration (from last access or write), asynchronous refresh, weak/soft references for keys and values, eviction notifications, and access statistics.

Core Classes and Parameters

Caffeine

: the base builder for creating high‑performance caches.

Key parameters:

maximumSize

: maximum number of entries.

maximumWeight

: maximum total weight (cannot be used with maximumSize).

initialCapacity

: initial cache capacity.

expireAfterWriteNanos

: expire after a given number of nanoseconds since write.

expireAfterAccessNanos

: expire after a given number of nanoseconds since last access.

refreshAfterWriteNanos

: refresh after a given number of nanoseconds since write.

2. Data Loading

Caffeine supports four loading strategies.

Manual loading

<code>public static void demo() {</code>
<code>    Cache<String, String> cache = Caffeine.newBuilder()</code>
<code>            .expireAfterAccess(Duration.ofMinutes(1))</code>
<code>            .maximumSize(100)</code>
<code>            .recordStats()</code>
<code>            .build();</code>
<code>    // put data</code>
<code>    cache.put("a", "a");</code>
<code>    // get if present</code>
<code>    String a = cache.getIfPresent("a");</code>
<code>    System.out.println(a);</code>
<code>    // get or load</code>
<code>    String b = cache.get("b", k -> {</code>
<code>        System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code>        try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code>        System.out.println("end query ...");</code>
<code>        return UUID.randomUUID().toString();</code>
<code>    });</code>
<code>    System.out.println(b);</code>
<code>    // invalidate</code>
<code>    cache.invalidate("a");</code>
<code>}</code>

Automatic loading

<code>public static void demo() {</code>
<code>    LoadingCache<String, String> loadingCache = Caffeine.newBuilder()</code>
<code>            .maximumSize(100)</code>
<code>            .expireAfterWrite(10, TimeUnit.MINUTES)</code>
<code>            .build(new CacheLoader<String, String>() {</code>
<code>                @Nullable @Override</code>
<code>                public String load(@NonNull String key) {</code>
<code>                    return createExpensiveValue();</code>
<code>                }</code>
<code>                @Override</code>
<code>                public @NonNull Map<String, String> loadAll(@NonNull Iterable<? extends String> keys) {</code>
<code>                    if (keys == null) { return Collections.emptyMap(); }</code>
<code>                    Map<String, String> map = new HashMap<>();</code>
<code>                    for (String key : keys) { map.put(key, createExpensiveValue()); }</code>
<code>                    return map;</code>
<code>                }</code>
<code>            });</code>
<code>    String a = loadingCache.get("a");</code>
<code>    System.out.println(a);</code>
<code>    Set<String> keys = new HashSet<>();</code>
<code>    keys.add("a"); keys.add("b");</code>
<code>    Map<String, String> allValues = loadingCache.getAll(keys);</code>
<code>    System.out.println(allValues);</code>
<code>}</code>
<code>private static String createExpensiveValue() {</code>
<code>    System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code>    try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code>    System.out.println("end query ...");</code>
<code>    return UUID.randomUUID().toString();</code>
<code>}</code>

A

LoadingCache

adds a

CacheLoader

to a regular cache.

The

getAll

method invokes

CacheLoader.load

for each missing key, and you can override

loadAll

for batch efficiency.

Manual asynchronous loading

<code>public static void demo() throws ExecutionException, InterruptedException {</code>
<code>    AsyncCache<String, String> asyncCache = Caffeine.newBuilder()</code>
<code>            .maximumSize(100)</code>
<code>            .buildAsync();</code>
<code>    asyncCache.put("a", CompletableFuture.completedFuture("a"));</code>
<code>    CompletableFuture<String> a = asyncCache.getIfPresent("a");</code>
<code>    System.out.println(a.get());</code>
<code>    CompletableFuture<String> cf = asyncCache.get("b", k -> createExpensiveValue("b"));</code>
<code>    System.out.println(cf.get());</code>
<code>    asyncCache.synchronous().invalidate("a");</code>
<code>    System.out.println(asyncCache.getIfPresent("a"));</code>
<code>}</code>
<code>private static String createExpensiveValue(String key) {</code>
<code>    System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code>    try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code>    System.out.println("end query ...");</code>
<code>    return UUID.randomUUID().toString();</code>
<code>}</code>

An

AsyncCache

generates cache entries on an

Executor

and returns

CompletableFuture

, allowing integration with reactive programming models. The

synchronous()

method blocks until the asynchronous computation completes.

The default thread pool is

ForkJoinPool.commonPool()

, but you can supply a custom executor via

Caffeine.executor(Executor)

.

Automatic asynchronous loading

<code>public static void demo() throws ExecutionException, InterruptedException {</code>
<code>    AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()</code>
<code>            .maximumSize(10_000)</code>
<code>            .expireAfterWrite(10, TimeUnit.MINUTES)</code>
<code>            .buildAsync((key, executor) -> createExpensiveValueAsync(key, executor));</code>
<code>    CompletableFuture<String> a = cache.get("a");</code>
<code>    System.out.println(a.get());</code>
<code>    Set<String> keys = new HashSet<>();</code>
<code>    keys.add("a"); keys.add("b");</code>
<code>    CompletableFuture<Map<String, String>> values = cache.getAll(keys);</code>
<code>    System.out.println(values.get());</code>
<code>}</code>
<code>private static String createExpensiveValue(String key) {</code>
<code>    System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code>    try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code>    System.out.println("end query ...");</code>
<code>    return UUID.randomUUID().toString();</code>
<code>}</code>
<code>private static CompletableFuture<String> createExpensiveValueAsync(String key, Executor executor) {</code>
<code>    System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code>    try { Thread.sleep(1000); executor.execute(() -> System.out.println("async create value....")); } catch (InterruptedException e) {}</code>
<code>    System.out.println("end query ...");</code>
<code>    return CompletableFuture.completedFuture(UUID.randomUUID().toString());</code>
<code>}</code>

An

AsyncLoadingCache

combines

AsyncCache

with an

AsyncCacheLoader

, suitable for asynchronous value generation.

3. Eviction Strategies

Caffeine offers three eviction strategies: size‑based, time‑based, and reference‑based, plus manual removal and listeners.

Size‑based eviction

<code>Cache<String, String> cache = Caffeine.newBuilder()</code>
<code>        .maximumSize(100)</code>
<code>        .recordStats()</code>
<code>        .build();</code>
<code>AsyncCache<String, String> asyncCache = Caffeine.newBuilder()</code>
<code>        .maximumWeight(10)</code>
<code>        .buildAsync();</code>

Time‑based eviction

<code>// Fixed time after last access</code>
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code>        .expireAfterAccess(Duration.ofMinutes(1))</code>
<code>        .recordStats()</code>
<code>        .build();</code>
<code>// Fixed time after write</code>
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code>        .expireAfterWrite(Duration.ofMinutes(1))</code>
<code>        .recordStats()</code>
<code>        .build();</code>
<code>// Custom expiry</code>
<code>Cache<String, String> expire = Caffeine.newBuilder()</code>
<code>        .expireAfter(new Expiry<String, String>() {</code>
<code>            @Override public long expireAfterCreate(...){ return LocalDateTime.now().plusMinutes(5).getSecond(); }</code>
<code>            @Override public long expireAfterUpdate(...){ return currentDuration; }</code>
<code>            @Override public long expireAfterRead(...){ return currentDuration; }</code>
<code>        })</code>
<code>        .recordStats()</code>
<code>        .build();</code>

Three time‑based methods:

expireAfterAccess(long, TimeUnit)

: evicts when a value has not been accessed for the specified duration.

expireAfterWrite(long, TimeUnit)

: evicts when a value has not been written or updated for the specified duration.

expireAfter(Expiry)

: evicts based on a custom

Expiry

implementation.

Reference‑based eviction

Reference types:

<code>// Weak keys and values</code>
<code>LoadingCache<Object, Object> weak = Caffeine.newBuilder()</code>
<code>        .weakKeys()</code>
<code>        .weakValues()</code>
<code>        .build(k -> createExpensiveValue());</code>
<code>// Soft values</code>
<code>LoadingCache<Object, Object> soft = Caffeine.newBuilder()</code>
<code>        .softValues()</code>
<code>        .build(k -> createExpensiveValue());</code>
weakKeys

and

weakValues

allow garbage collection when no strong references exist;

softValues

are reclaimed under memory pressure.

Manual removal

<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code>        .expireAfterWrite(Duration.ofMinutes(1))</code>
<code>        .recordStats()</code>
<code>        .build();</code>
<code>// single</code>
<code>cache.invalidate("a");</code>
<code>// bulk</code>
<code>Set<String> keys = new HashSet<>(); keys.add("a"); keys.add("b");</code>
<code>cache.invalidateAll(keys);</code>
<code>// all</code>
<code>cache.invalidateAll();</code>

Removal listeners

<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code>        .expireAfterWrite(Duration.ofMinutes(1))</code>
<code>        .recordStats()</code>
<code>        .evictionListener((key, value, cause) -> System.out.println("evict cause " + cause))</code>
<code>        .removalListener((key, value, cause) -> System.out.println("removed cause " + cause))</code>
<code>        .build();</code>

Listeners run asynchronously using the default

ForkJoinPool.commonPool()

, but a custom executor can be supplied via

Caffeine.executor(Executor)

.

Common eviction causes:

EXPLICIT – manual removal.

REPLACED – entry replaced by a new put.

COLLECTED – garbage collection of weak/soft references.

EXPIRED – time‑based expiration.

SIZE – size‑based eviction.

4. Cache Statistics

Calling

Caffeine.recordStats()

enables collection of metrics such as hit count, hit rate, miss count, miss rate, load success/failure counts, total load time, eviction count, eviction weight, request count, and average load penalty.

<code>CacheStats stats = cache.stats();</code>
<code>System.out.println("stats.hitCount():" + stats.hitCount());</code>
<code>System.out.println("stats.hitRate():" + stats.hitRate());</code>
<code>System.out.println("stats.missCount():" + stats.missCount());</code>
<code>System.out.println("stats.missRate():" + stats.missRate());</code>
<code>System.out.println("stats.loadSuccessCount():" + stats.loadSuccessCount());</code>
<code>System.out.println("stats.loadFailureCount():" + stats.loadFailureCount());</code>
<code>System.out.println("stats.loadFailureRate():" + stats.loadFailureRate());</code>
<code>System.out.println("stats.totalLoadTime():" + stats.totalLoadTime());</code>
<code>System.out.println("stats.evictionCount():" + stats.evictionCount());</code>
<code>System.out.println("stats.evictionWeight():" + stats.evictionWeight());</code>
<code>System.out.println("stats.requestCount():" + stats.requestCount());</code>
<code>System.out.println("stats.averageLoadPenalty():" + stats.averageLoadPenalty());</code>
backendJavaPerformanceCacheCaffeineEviction
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

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.