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.
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
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>}Automatic loading
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>}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
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>}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
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>}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
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();Time‑based eviction
// 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();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:
// 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()); weakKeysand weakValues allow garbage collection when no strong references exist; softValues are reclaimed under memory pressure.
Manual removal
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();Removal listeners
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();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.
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());Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
