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.

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 maximumSize or maximumWeight — unbounded caches cause OOM
  • Use expireAfterWrite for data that changes; expireAfterAccess for 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