On this page
Local Cache
Local (in-process) caches store frequently accessed data in JVM heap memory, eliminating network round trips and reducing database load. They are the fastest cache layer but limited to a single application instance.
Caffeine (Recommended)
High-performance caching library, successor to Guava Cache:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
LoadingCache<Long, User> userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.expireAfterAccess(Duration.ofMinutes(5))
.recordStats()
.build(userId -> userRepository.findById(userId).orElseThrow());
User user = userCache.get(123L);
// Stats
CacheStats stats = userCache.stats();
System.out.println("Hit rate: " + stats.hitRate());
Cache Policies
| Policy | Method | Use case |
|---|---|---|
| Size-based eviction | maximumSize(n) |
Limit memory usage |
| Weight-based | maximumWeight(w) |
Variable-size entries |
| Time-based (write) | expireAfterWrite(duration) |
Data changes on schedule |
| Time-based (access) | expireAfterAccess(duration) |
Infrequently used data |
| Reference-based | weakKeys(), softValues() |
GC-aware eviction |
Spring Cache with Caffeine
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("users", "products");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { return userRepository.findById(id).orElseThrow(); }
@CacheEvict(value = "users", key = "#user.id")
public User update(User user) { return userRepository.save(user); }
@CacheEvict(value = "users", allEntries = true)
public void clearCache() { }
}
Guava Cache (Legacy)
Still widely used in existing codebases:
Cache<String, Product> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
Product product = cache.get("SKU-123", () -> productRepository.findBySku("SKU-123"));
Prefer Caffeine for new projects — better performance and active maintenance.
Cache-Aside Pattern
public User getUser(Long id) {
User cached = userCache.getIfPresent(id);
if (cached != null) return cached;
User user = userRepository.findById(id).orElseThrow();
userCache.put(id, user);
return user;
}
Spring @Cacheable implements this automatically.
Local vs Distributed Cache
| Feature | Local (Caffeine) | Distributed (Redis) |
|---|---|---|
| Latency | Nanoseconds | Milliseconds |
| Consistency | Per-instance | Shared across instances |
| Size limit | JVM heap | External memory |
| Invalidation | Per-instance | Global |
| Best for | Read-heavy, same data | Shared state, sessions |
Best Practices
- Always set
maximumSizeormaximumWeight— unbounded caches cause OOM - Use
expireAfterWritefor data that changes;expireAfterAccessfor hot data - Enable
recordStats()and monitor hit rate — below 80% means poor cache design - Use local cache for reference data; distributed cache for shared mutable state
- Invalidate on writes — stale cache is worse than no cache