The Ultimate Caffeine Cache Cheatsheet: Boost Performance & Reduce Response Times

Introduction: What is Caffeine Cache and Why it Matters

Caffeine is a high-performance, near-optimal caching library for Java developed by Google. It replaced Guava’s cache due to its superior efficiency and flexibility. Caffeine matters because it provides an in-memory cache with excellent hit rates, minimal overhead, and concurrent operations without locking, making it ideal for high-throughput, low-latency applications.

Core Concepts & Principles

Key Cache Terminology

  • Cache: Temporary storage of data for faster future access
  • Cache Hit: When requested data is found in cache
  • Cache Miss: When requested data is not in cache
  • Eviction: Process of removing entries from cache
  • TTL (Time-to-Live): Maximum lifetime of a cache entry
  • LRU (Least Recently Used): Eviction policy based on recent usage

Caffeine’s Core Features

  • High-performance, memory-efficient design
  • Window TinyLFU eviction policy (excellent hit rate)
  • Concurrent operations without blocking
  • Flexible loading strategies (synchronous, asynchronous)
  • Rich statistics and monitoring
  • Automatic entry expiration (time-based and reference-based)

Implementation: Getting Started

Maven Dependency

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

Gradle Dependency

implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

Basic Cache Creation

// Manual cache population
Cache<Key, Value> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .build();

// Synchronous loading cache
LoadingCache<Key, Value> loadingCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .build(key -> createValue(key));

// Asynchronous loading cache
AsyncLoadingCache<Key, Value> asyncLoadingCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .buildAsync(key -> createValueAsync(key));

Cache Configuration Options

Size-Based Eviction

// Maximum size (count of entries)
.maximumSize(10_000)

// Maximum weight (custom weighting)
.maximumWeight(10_000)
.weigher((key, value) -> value.getWeight())

Time-Based Expiration

// Expire after write
.expireAfterWrite(Duration.ofMinutes(10))

// Expire after access
.expireAfterAccess(Duration.ofMinutes(5))

// Variable expiration
.expireAfter(new Expiry<Key, Value>() {
    public long expireAfterCreate(Key key, Value value, long currentTime) {
        return timeUnit.toNanos(1);
    }
    public long expireAfterUpdate(Key key, Value value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    public long expireAfterRead(Key key, Value value, long currentTime, long currentDuration) {
        return currentDuration;
    }
})

Reference-Based Eviction

// Weak keys (allows GC of keys)
.weakKeys()

// Weak values (allows GC of values)
.weakValues()

// Soft values (allows GC of values when memory is constrained)
.softValues()

Listeners & Stats

// Removal listener
.removalListener((key, value, cause) -> 
    System.out.printf("Key %s was removed due to %s\n", key, cause))

// Record stats
.recordStats()

Cache Operations

Basic Operations

// Manual cache
Cache<Key, Value> cache = Caffeine.newBuilder().build();

// Insert/update
cache.put(key, value);

// Retrieve (returns null if absent)
Value value = cache.getIfPresent(key);

// Get or compute
Value value = cache.get(key, k -> createValue(k));

// Invalidate
cache.invalidate(key);

// Bulk operations
cache.putAll(map);
cache.invalidateAll(keys);
cache.invalidateAll();

Loading Cache Operations

LoadingCache<Key, Value> cache = Caffeine.newBuilder()
    .build(key -> createValue(key));

// Get (computes if absent)
Value value = cache.get(key);

// Get all (computes missing values)
Map<Key, Value> map = cache.getAll(keys);

// Refresh (asynchronously reload)
cache.refresh(key);

Async Cache Operations

AsyncLoadingCache<Key, Value> cache = Caffeine.newBuilder()
    .buildAsync(key -> createValueAsync(key));

// Get as CompletableFuture
CompletableFuture<Value> future = cache.get(key);

// Get all as CompletableFuture
CompletableFuture<Map<Key, Value>> future = cache.getAll(keys);

Cache Statistics & Monitoring

Accessing Stats

Cache<Key, Value> cache = Caffeine.newBuilder()
    .recordStats()
    .build();

// Get stats
CacheStats stats = cache.stats();

// Access specific metrics
long hitCount = stats.hitCount();
long missCount = stats.missCount();
double hitRate = stats.hitRate();
long evictionCount = stats.evictionCount();

Key Stats Metrics

MetricDescription
hitCount()Number of cache hits
missCount()Number of cache misses
loadSuccessCount()Number of successful loads
loadFailureCount()Number of failed loads
totalLoadTime()Total loading time (nanoseconds)
evictionCount()Number of evictions
evictionWeight()Weight of evicted entries
hitRate()Ratio of cache hits to requests
averageLoadPenalty()Average time to load a value

Comparison of Cache Types

FeatureManual CacheLoading CacheAsync Loading Cache
PopulationManualAutomaticAutomatic
Missing ValueReturns nullComputes valueReturns CompletableFuture
Bulk LoadingNoYesYes
RefreshNoYesYes
ExecutionN/ABlockingNon-blocking
Best forSimple cachingMost use casesHigh concurrency

Common Challenges & Solutions

Challenge: Cache Stampede (Thundering Herd)

Problem: Multiple threads try to load the same key simultaneously
Solution:

  • Use LoadingCache or AsyncLoadingCache which handles concurrent loads
  • Consider refreshAfterWrite() to proactively refresh entries

Challenge: Memory Pressure

Problem: Cache consumes too much memory
Solution:

  • Use maximumSize() or maximumWeight()
  • Consider softValues() for memory-sensitive caches
  • Monitor with stats() and adjust size accordingly

Challenge: Stale Data

Problem: Cache entries become outdated
Solution:

  • Use expireAfterWrite() for time-based expiration
  • Use refreshAfterWrite() for background refresh
  • Implement CacheLoader.reload() for smart refreshing

Challenge: Cache Poisoning

Problem: Invalid values getting cached
Solution:

  • Validate values before caching
  • Use short TTLs for potentially volatile data
  • Implement health checks with selective invalidation

Best Practices & Tips

Performance Optimization

  • Size your cache appropriately (monitor hit/miss rates)
  • Use asynchronous loading for slow data sources
  • Consider preloading frequently accessed entries
  • Warm up the cache before heavy traffic

Configuration Tips

  • Set reasonable expiration times based on data volatility
  • Use weakKeys() when keys have object identity semantics
  • Choose between softValues() and weakValues() based on memory constraints
  • Favor maximumSize() over maximumWeight() unless weight varies significantly

Testing & Development

  • Use FakeTicker for time-based testing
  • Enable recordStats() during development and testing
  • Consider a smaller cache size in tests for faster eviction

Spring Integration

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "products");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats());
        return cacheManager;
    }
}

Advanced Techniques

Custom Weigher

Caffeine.newBuilder()
    .maximumWeight(10_000)
    .weigher((String key, DataObject value) -> value.getSize())
    .build();

Custom CacheLoader

CacheLoader<Key, Value> loader = new CacheLoader<Key, Value>() {
    @Override
    public Value load(Key key) {
        return getValueFromSource(key);
    }
    
    @Override
    public Map<Key, Value> loadAll(Set<? extends Key> keys) {
        return getMultipleValuesFromSource(keys);
    }
    
    @Override
    public Value reload(Key key, Value oldValue) {
        return refreshValue(key, oldValue);
    }
};

Writer-Through Cache

Cache<Key, Value> cache = Caffeine.newBuilder()
    .writer(new CacheWriter<Key, Value>() {
        @Override
        public void write(Key key, Value value) {
            storage.put(key, value);
        }
        
        @Override
        public void delete(Key key, Value value, RemovalCause cause) {
            if (cause.wasEvicted()) {
                storage.delete(key);
            }
        }
    })
    .build();

Resources for Further Learning

Official Documentation

Books & Articles

Tools & Extensions

Community Resources

  • Stack Overflow caffeine-cache tag
  • Java Performance & Scalability community forums
  • GitHub Discussions on the Caffeine repository
Scroll to Top