Published on Fri Mar 07 2025 12:00:00 GMT+0000 (Coordinated Universal Time) by Purusothaman Ramanujam
Google Guava Caching: Local Caching with Eviction Strategies
Introduction
Google Guava provides a powerful caching framework that allows you to create local caches with various eviction strategies. Caching is essential for improving application performance by storing frequently accessed data in memory.
In this post, we’ll explore Guava’s caching capabilities, including different eviction strategies, statistics, and best practices.
Adding Guava to Your Project
Maven Dependency
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version></dependency>
Gradle Dependency
implementation 'com.google.guava:guava:32.1.3-jre'
Basic Caching
Guava’s Cache interface provides a simple way to create local caches:
import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import java.util.concurrent.TimeUnit;
public class BasicCachingExample { public static void main(String[] args) { // Create a simple cache Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) // Maximum 100 entries .build();
// Put values in cache cache.put("key1", "value1"); cache.put("key2", "value2");
// Get values from cache String value1 = cache.getIfPresent("key1"); System.out.println(value1); // "value1"
// Get with default value if not present String value3 = cache.get("key3", () -> "default_value"); System.out.println(value3); // "default_value"
// Check cache size System.out.println("Cache size: " + cache.size()); }}
Eviction Strategies
Size-Based Eviction
Limit the cache size to prevent memory issues:
public class SizeBasedEvictionExample { public static void main(String[] args) { Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(3) // Keep only 3 entries .build();
cache.put("key1", "value1"); cache.put("key2", "value2"); cache.put("key3", "value3"); cache.put("key4", "value4"); // This will evict key1
System.out.println(cache.getIfPresent("key1")); // null System.out.println(cache.getIfPresent("key4")); // "value4" }}
Time-Based Eviction
Evict entries based on time:
public class TimeBasedEvictionExample { public static void main(String[] args) { Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) // Expire after 10 minutes .expireAfterAccess(5, TimeUnit.MINUTES) // Expire after 5 minutes of no access .build();
cache.put("key1", "value1");
// Entry will be automatically removed after 10 minutes // or 5 minutes of no access, whichever comes first }}
Weight-Based Eviction
Use custom weights for eviction:
import com.google.common.cache.Weigher;
public class WeightBasedEvictionExample { public static void main(String[] args) { Cache<String, String> cache = CacheBuilder.newBuilder() .maximumWeight(1000) // Maximum weight of 1000 .weigher(new Weigher<String, String>() { @Override public int weigh(String key, String value) { return key.length() + value.length(); // Weight based on string lengths } }) .build();
cache.put("short", "value"); // Weight: 5 + 5 = 10 cache.put("very_long_key_name", "very_long_value_string"); // Weight: 18 + 22 = 40
System.out.println("Cache size: " + cache.size()); }}
Loading Cache
A LoadingCache automatically loads values when they’re not present:
import com.google.common.cache.LoadingCache;import com.google.common.cache.CacheLoader;
public class LoadingCacheExample { public static void main(String[] args) { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // This method is called when a key is not found return "Computed value for: " + key; } });
// Automatically loads the value if not present String value = cache.get("key1"); System.out.println(value); // "Computed value for: key1"
// Get multiple values at once Map<String, String> values = cache.getAll(Arrays.asList("key1", "key2", "key3")); System.out.println(values); }}
Cache Statistics
Monitor cache performance with statistics:
public class CacheStatisticsExample { public static void main(String[] args) { Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .recordStats() // Enable statistics .build();
// Perform some operations cache.put("key1", "value1"); cache.getIfPresent("key1"); // Hit cache.getIfPresent("key2"); // Miss
// Get statistics System.out.println("Cache stats: " + cache.stats()); System.out.println("Hit rate: " + cache.stats().hitRate()); System.out.println("Miss rate: " + cache.stats().missRate()); System.out.println("Load count: " + cache.stats().loadCount()); System.out.println("Eviction count: " + cache.stats().evictionCount()); }}
Advanced Caching Patterns
Cache with Expensive Computation
public class ExpensiveComputationCache { private final LoadingCache<String, String> cache;
public ExpensiveComputationCache() { this.cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(1, TimeUnit.HOURS) .recordStats() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return performExpensiveComputation(key); } }); }
private String performExpensiveComputation(String key) { // Simulate expensive computation try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Computed result for: " + key; }
public String getValue(String key) { try { return cache.get(key); } catch (ExecutionException e) { throw new RuntimeException("Failed to load value for key: " + key, e); } }
public void printStats() { System.out.println("Cache stats: " + cache.stats()); }}
Cache with Refresh
public class RefreshableCacheExample { public static void main(String[] args) { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(30, TimeUnit.MINUTES) // Refresh after 30 minutes .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return fetchFromDatabase(key); }
@Override public ListenableFuture<String> reload(String key, String oldValue) throws Exception { // Asynchronous refresh return executor.submit(() -> fetchFromDatabase(key)); } });
// Manual refresh cache.refresh("key1"); }
private static String fetchFromDatabase(String key) { // Simulate database fetch return "Database value for: " + key; }}
Cache Removal Listeners
Monitor cache evictions with removal listeners:
import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;
public class CacheRemovalListenerExample { public static void main(String[] args) { Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(2) .removalListener(new RemovalListener<String, String>() { @Override public void onRemoval(RemovalNotification<String, String> notification) { System.out.println("Removed: " + notification.getKey() + " -> " + notification.getValue() + " (Reason: " + notification.getCause() + ")"); } }) .build();
cache.put("key1", "value1"); cache.put("key2", "value2"); cache.put("key3", "value3"); // This will evict key1
// Manual removal cache.invalidate("key2"); }}
Best Practices
- Choose Appropriate Eviction Strategy: Use size-based for memory constraints, time-based for data freshness
- Monitor Cache Performance: Enable statistics and monitor hit rates
- Handle Exceptions: Always handle ExecutionException from LoadingCache
- Use LoadingCache for Expensive Operations: Automatically load missing values
- Set Reasonable Limits: Avoid unbounded caches to prevent memory issues
- Consider Refresh: Use refreshAfterWrite for data that can be stale
Common Pitfalls
- Memory Leaks: Not setting maximum size or weight limits
- Ignoring Statistics: Not monitoring cache performance
- Blocking Operations: Performing blocking operations in CacheLoader
- Not Handling Exceptions: Forgetting to handle ExecutionException
- Inappropriate Eviction: Using wrong eviction strategy for your use case
Performance Considerations
- Memory Usage: Monitor cache memory consumption
- Hit Rate: Aim for high hit rates (80%+) for good performance
- Concurrency: Guava caches are thread-safe and highly concurrent
- Statistics Overhead: Statistics have minimal performance impact
- Eviction Performance: LRU eviction is very fast
Conclusion
Guava’s caching framework provides a powerful and flexible way to implement local caching in your applications. With various eviction strategies, statistics monitoring, and automatic loading capabilities, it’s suitable for a wide range of caching needs.
Start with simple size-based eviction and gradually explore more advanced features like time-based eviction, statistics, and removal listeners as your requirements grow. Proper caching can significantly improve your application’s performance and user experience.
Resources
Written by Purusothaman Ramanujam
← Back to blog