Understanding Caffeine: A High‑Performance Java Caching Library

This article introduces the Caffeine Java caching library, detailing its features, core classes and parameters, various loading strategies—including manual, automatic, and asynchronous loading—eviction policies, removal listeners, and cache statistics, accompanied by practical code examples for integration in backend applications.

JD Tech Talk
JD Tech Talk
JD Tech Talk
Understanding Caffeine: A High‑Performance Java Caching Library

Understanding Caffeine

Caffeine is a Java‑8 based high‑performance caching library that offers near‑optimal hit rates and is the default local cache implementation in Spring Boot. It provides flexible builders to create caches with automatic loading (optional async), size‑based eviction, time‑based expiration, asynchronous refresh, weak/soft references for keys and values, eviction notifications, and access statistics.

Automatic loading of entries (optional asynchronous)

Size‑based eviction

Expiration after write or access

Asynchronous refresh

Weakly referenced keys and weak/softly referenced values

Eviction notifications

Cache access statistics

Core classes include Caffeine for building caches and parameters such as maximumSize, maximumWeight, initialCapacity, expireAfterWriteNanos, expireAfterAccessNanos, and refreshAfterWriteNanos.

Data Loading Strategies

Caffeine supports four loading strategies:

Manual loading

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

Automatic loading

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

Manual asynchronous loading

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

Automatic asynchronous loading

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

Eviction Strategies

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

Size‑based eviction using .maximumSize() or .maximumWeight() Time‑based eviction via .expireAfterAccess(), .expireAfterWrite(), or a custom Expiry implementation

Reference‑based eviction with .weakKeys(), .weakValues(), and .softValues() Manual removal can be performed with cache.invalidate(key), cache.invalidateAll(keys), or cache.invalidateAll(). Removal listeners can be registered via .removalListener() or .evictionListener() to react to eviction causes such as EXPLICIT, REPLACED, COLLECTED, EXPIRED, or SIZE.

Cache Statistics

Enabling .recordStats() allows collection of detailed metrics such as hit count, miss count, load success/failure counts, total load time, eviction count, and request count.

CacheStats stats = cache.stats();
System.out.println("stats.hitCount():" + stats.hitCount());
System.out.println("stats.hitRate():" + stats.hitRate());
System.out.println("stats.missCount():" + stats.missCount());
System.out.println("stats.missRate():" + stats.missRate());
System.out.println("stats.loadSuccessCount():" + stats.loadSuccessCount());
System.out.println("stats.loadFailureCount():" + stats.loadFailureCount());
System.out.println("stats.totalLoadTime():" + stats.totalLoadTime());
System.out.println("stats.evictionCount():" + stats.evictionCount());
System.out.println("stats.requestCount():" + stats.requestCount());
System.out.println("stats.averageLoadPenalty():" + stats.averageLoadPenalty());
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.

performanceCacheCaffeineAsync
JD Tech Talk
Written by

JD Tech Talk

Official JD Tech public account delivering best practices and technology innovation.

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.