Backend Development 15 min read

Understanding and Configuring Caffeine Cache in Java Applications

Understanding Caffeine Cache in Java involves using its builder for options like expiration, refresh, and weight‑based eviction, recognizing that configuring both expireAfterWrite and expireAfterAccess is redundant, grasping core methods such as isBounded, refreshes, computeIfAbsent, and avoiding common pitfalls like mis‑ordered expiration settings, blocking loaders, cache penetration, and mutable cached objects.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Understanding and Configuring Caffeine Cache in Java Applications

This article introduces Caffeine Cache, a high‑performance local cache library that has become the default caching solution in Spring Boot 2.0, replacing Google Guava. It explains why fast, multi‑level caching is essential for recommendation services, where a higher "completion rate" directly translates to better algorithmic results and commercial revenue.

The author shares practical knowledge gained from using Caffeine, including common pitfalls such as misconfiguration that can make the cache as slow as disk I/O. The article is organized into three main parts: an overview, configuration patterns, and deep‑dive into the core implementation.

1. Configuration patterns

Besides the usual Spring annotations (@EnableCache, @Cacheable), Caffeine can be used directly via Caffeine.newBuilder().build() to create a LoadingCache . The builder offers many options, illustrated by the following code snippet:

public Caffeine<K, V> maximumWeight(@NonNegative long maximumWeight) {
requireState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", this.maximumWeight);
requireState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize);
this.maximumWeight = maximumWeight;
requireArgument(maximumWeight >= 0, "maximum weight must not be negative");
return this;
}

Key builder methods include expireAfterWrite , expireAfterAccess , refreshAfterWrite , and weight‑based eviction. The article warns that configuring both expireAfterWrite and expireAfterAccess together is unnecessary because the latter already covers the former.

2. Core implementation insights

The article walks through the internal classes BoundedLocalCache and UnboundedLocalCache . Whether a cache is bounded is determined by the isBounded() method:

boolean isBounded() {
return (maximumSize != UNSET_INT) || (maximumWeight != UNSET_INT) ||
(expireAfterAccessNanos != UNSET_INT) || (expireAfterWriteNanos != UNSET_INT) ||
(expiry != null) || (keyStrength != null) || (valueStrength != null);
}

The refreshes() method indicates if refreshAfterWrite is configured:

boolean refreshes() {
// Returns true when refreshAfterWrite is set
return refreshNanos != UNSET_INT;
}

The central computeIfAbsent logic (simplified) is shown below:

public @Nullable V computeIfAbsent(K key, Function
mappingFunction, boolean recordStats, boolean recordLoad) {
requireNonNull(key);
requireNonNull(mappingFunction);
long now = expirationTicker().read();
Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
if (node != null) {
V value = node.getValue();
if (value != null && !hasExpired(node, now)) {
afterRead(node, now, recordStats);
return value;
}
}
// Load value when absent or expired
return doComputeIfAbsent(key, nodeFactory.newReferenceKey(key, keyReferenceQueue()), mappingFunction, new long[]{now}, recordStats);
}

The hasExpired check combines the three possible expiration policies:

boolean hasExpired(Node<K, V> node, long now) {
return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
|| (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
|| (expiresVariable() && (now - node.getVariableTime() >= 0));
}

Understanding these methods helps developers avoid common mistakes, such as calling builder methods twice (which throws an exception) or configuring both expiration and refresh incorrectly.

3. Practical pitfalls and best practices

Do not set expireAfterWrite shorter than refreshAfterWrite ; otherwise refresh never triggers.

When CacheLoader#load is slow, it blocks the calling thread; using asyncReload with refreshAfterWrite mitigates impact.

Avoid cache penetration by caching a placeholder object for null results.

Prevent accidental mutation of cached objects by returning defensive copies from get .

Handle load failures by returning the previous value in asyncReload or using a sentinel object.

During high concurrency, only the first thread performs the actual load; subsequent threads wait for the compute to finish, reducing overall latency.

The article concludes with a list of reference links for deeper reading on Caffeine’s eviction policies, ticker mechanisms, writer interfaces, and performance characteristics.

JavaperformanceConcurrencylocal cacheCaffeine CacheCache EvictionCache Configuration
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

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.