SuperAPI AI Directory

caffeine-patterns

Coding & development

View repository

Updated 10-15 21:48

About this skill

Caffeine JVM local cache

Source:https://github.com/ben-manes/caffeine/wiki
GitHub:https://github.com/ben-manes/caffeine

Capability Boundaries

✅ Strong Suits

  1. Local vs remote choice — Use Caffeine for high-frequency data in a single service, use Redis for sharing across multiple services
  2. Expiration strategies — expireAfterWrite (after write) vs expireAfterAccess (after access) vs refreshAfterWrite (asynchronous refresh)
  3. CacheLoader — Automatic loading of missing data + reload asynchronous refresh + graceful degradation (return old value)
  4. Spring Cache integration — @Cacheable/@CacheEvict + CaffeineCacheManager
  5. Eviction strategies — maximumSize (count)/maximumWeight (weight)/Window TinyLFU
  6. Record statistics — recordStats() + CacheStats to monitor hit rate

❌ Out of Scope

  1. Distributed cache/shared data → redis-redisson-patterns
  2. Redis + Caffeine two-level cache → redis-redisson-patterns(already covered)

Expiration strategy selection decision

Strategy Behavior Applicable scenarios
expireAfterWrite Expires after a fixed time following write ✅ Recommended (most business cases), predictable data refresh
expireAfterAccess Expires after a fixed time following last access Session cache/user login status
refreshAfterWrite Asynchronously refreshes after a fixed time following write (returns old value) Non-blocking refresh for hot data
expireAfterWrite + refreshAfterWrite Refresh executes before expiration, never blocks ⭐ Recommended combination

⚠️ refreshAfterWrite must be < expireAfterWrite, otherwise refresh will never be triggered

The mistakes LLMs make most often

# Mistake Correct approach
1 ConcurrentHashMap<String, Object> + manual expiration Caffeine CacheBuilder Window TinyLFU algorithm
2 All caches go through Redis High-frequency caching in a single service goes through Caffeine (nanosecond-level, Redis is millisecond-level)
3 Not setting maximumSize maximumSize(10000) to prevent memory overflow
4 Not handling loading exceptions after setting refreshAfterWrite The old value is still usable; a load failure does not affect the service
5 refreshAfterWrite > expireAfterWrite Never refresh (it is simply deleted on expiration)
6 CacheLoader.load() returns null Returning null = cache penetration (every query goes through load)
7 Setting only refreshAfterWrite without setting maximumSize Unbounded cache → OOM
8 Modifying the object returned by the cache The cached object is modified externally → data inconsistency

Core patterns

Pattern 1: Common Caffeine configuration (recommended combination)

// ✅ expireAfterWrite + refreshAfterWrite 组合(推荐)
Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)   // 5分钟过期
    .refreshAfterWrite(3, TimeUnit.MINUTES)  // 3分钟后台刷新(返回旧值)
    .recordStats()                            // 记录命中率
    .build(key -> loadFromDB(key));           // CacheLoader

Pattern 2: CacheLoader + reload graceful degradation

LoadingCache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .refreshAfterWrite(3, TimeUnit.MINUTES)
    .build(new CacheLoader<String, User>() {
        @Override
        public @Nullable User load(String key) {
            // 缓存未命中时加载
            User user = userMapper.selectById(Long.parseLong(key));
            // ✅ 不要返回 null!使用 Optional 或空对象占位
            return user != null ? user : NULL_USER;
        }

        @Override
        public @Nullable User reload(String key, User oldValue) {
            // ✅ 刷新时优雅降级:加载失败返回旧值
            try {
                return load(key);
            } catch (Exception e) {
                log.error("刷新缓存失败, key={}", key, e);
                return oldValue;  // ← 返回旧值,不影响服务
            }
        }
    });

// 使用
User user = cache.get("user:1");  // 自动加载

Pattern 3: Spring Cache integration

@Configuration
@EnableCaching  // ← 必须!
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager mgr = new CaffeineCacheManager();
        mgr.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .recordStats());
        return mgr;
    }
}

@Service
public class UserService {
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUser(Long id) {
        return userMapper.selectById(id);
    }

    @CacheEvict(value = "users", key = "#id")
    public void updateUser(Long id, UserDTO dto) {
        userMapper.updateById(dto.toEntity(id));
    }

    @CacheEvict(value = "users", allEntries = true)  // 清空整个缓存
    public void refreshAll() { }
}

Pattern 4: Asynchronous loading (AsyncCache)

AsyncCache<String, User> asyncCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .refreshAfterWrite(1, TimeUnit.MINUTES)  // 异步刷新
    .buildAsync(key -> loadFromDB(key));

// 异步获取
CompletableFuture<User> future = asyncCache.get("user:1");
future.thenAccept(user -> System.out.println(user));

Pattern 5: Manual cache operations + statistics

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .recordStats()  // 启用统计
    .build();

// 手动写入
cache.put("user:1", user);
// 手动失效
cache.invalidate("user:1");

// 统计数据
CacheStats stats = cache.stats();
log.info("命中率: {}, 加载次数: {}, 驱逐次数: {}",
    stats.hitRate(), stats.loadCount(), stats.evictionCount());

Gotchas

  1. Caffeine is an in-process cache — In a multi-instance deployment, each instance has its own independent cache, causing data inconsistency
  2. expireAfterAccess is suitable for session caching — If the post-write frequency is very high, set access
  3. refreshAfterWrite must be < expireAfterWrite — Otherwise refresh is meaningless
  4. CacheLoader.load() must not return null — Return an empty placeholder object or throw an exception
  5. Spring Cache's @Cacheable return value must be serializable — If Redis is used as the backend
  6. maximumSize eviction may lose data — Use a database as the ultimate safeguard for important data
  7. Working with Redisson — Caffeine (L1) + Redis (L2) is a classic two-level cache combination
  8. Be mindful of defensive copying when cached objects are mutable — External modification of cached objects causes data inconsistency
  9. recordStats() does not affect performance — Recommended to enable in production
  10. expireAfter(Expiry) supports custom expiration times — Dynamically determine TTL based on the value
  11. Caffeine 3.x and 2.x compatibility — Spring Boot 2.x uses Caffeine 2.x, Spring Boot 3.x uses 3.x
  12. Use weakKeys/softValues with caution — GC-triggered eviction is unpredictable

Data Privacy

This skill does not collect, store, or transmit any user data.