How to Implement Custom Guava Caches: A Complete Guide to CacheBuilder Extensions

Implement custom Guava caches by extending CacheLoader for loading logic, implementing Weigher for size-based eviction, providing RemovalListener for eviction callbacks, and assembling the configuration via CacheBuilder to create specialized LoadingCache instances.

The Google Guava library (google/guava) provides a robust caching framework centered on the CacheBuilder and CacheLoader abstractions found in the com.google.common.cache package. To implement custom Guava caches with specialized data loading, eviction strategies, or monitoring capabilities, developers utilize specific extension points defined in the source code. This guide demonstrates how to leverage these APIs—referencing actual file paths and method signatures from the Guava repository—to build production-ready caching solutions.

Customizing Data Loading with CacheLoader

The primary mechanism for defining how missing values are computed or retrieved is the CacheLoader abstract class located in guava/src/com/google/common/cache/CacheLoader.java. Subclasses implement the load(K key) method to define synchronous loading logic, and optionally override loadAll(Iterable<? extends K> keys) for batch retrieval.

For caches requiring automatic loading, pass your CacheLoader implementation to CacheBuilder.build():

import com.google.common.cache.*;

public class UserCache {
    private final LoadingCache<String, User> cache;

    public UserCache() {
        cache = CacheBuilder.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(15, java.util.concurrent.TimeUnit.MINUTES)
            .recordStats()
            .build(new CacheLoader<String, User>() {
                @Override
                public User load(String userId) throws Exception {
                    return fetchUserFromDataSource(userId);
                }
            });
    }

    public User get(String userId) throws ExecutionException {
        return cache.get(userId);
    }

    private User fetchUserFromDataSource(String userId) {
        return new User(userId, "Name-" + userId);
    }
}

Implementing Weight-Based Eviction with Weigher

When entries vary significantly in memory footprint, use the Weigher<K,V> interface to define custom weights instead of simple entry counts. Configure this in guava/src/com/google/common/cache/CacheBuilder.java via weigher(Weigher<? super K, ? super V> weigher), paired with maximumWeight(long maximumWeight).

The weigher calculates an integer weight for each entry. Eviction occurs when the total weight exceeds the configured maximum:

import com.google.common.cache.*;

public class WeightedCache {
    private final Cache<String, byte[]> cache;

    public WeightedCache() {
        cache = CacheBuilder.newBuilder()
            .maximumWeight(100_000_000) // 100 MB total weight
            .weigher((String key, byte[] value) -> value.length)
            .removalListener((RemovalNotification<String, byte[]> notification) -> {
                System.out.println("Evicted key: " + notification.getKey()
                                   + " because of " + notification.getCause());
            })
            .build();
    }

    public void put(String key, byte[] data) {
        cache.put(key, data);
    }

    public byte[] getIfPresent(String key) {
        return cache.getIfPresent(key);
    }
}

Handling Removal Events with RemovalListener

To react when entries are evicted, expired, or explicitly removed, provide a RemovalListener<K,V> via CacheBuilder.removalListener(). The listener receives a RemovalNotification<K,V> object containing the key, value, and RemovalCause.

This hook is essential for resource cleanup, logging, or triggering downstream updates when cache entries disappear. Register the listener during cache construction to capture all removal events.

Configuring Asynchronous Refresh Policies

For entries that should be periodically refreshed without blocking readers, override reload(K key, V oldValue) in your CacheLoader. Enable this behavior with refreshAfterWrite(long duration, TimeUnit unit) in the builder.

The reload() method returns a ListenableFuture<V>, allowing asynchronous computation using Guava's concurrency utilities:

import com.google.common.cache.*;

public class RefreshingCache {
    private final LoadingCache<Integer, ComputationResult> cache;

    public RefreshingCache() {
        cache = CacheBuilder.newBuilder()
            .refreshAfterWrite(5, java.util.concurrent.TimeUnit.MINUTES)
            .build(new CacheLoader<Integer, ComputationResult>() {
                @Override
                public ComputationResult load(Integer key) {
                    return computeExpensiveResult(key);
                }

                @Override
                public ListenableFuture<ComputationResult> reload(
                        Integer key, ComputationResult oldValue) {
                    return Futures.immediateFuture(computeExpensiveResult(key));
                }
            });
    }

    private ComputationResult computeExpensiveResult(Integer key) {
        return new ComputationResult(key, Math.sqrt(key));
    }
}

Monitoring Cache Statistics

Enable performance tracking by calling recordStats() on the CacheBuilder. Query the resulting CacheStats object (defined in guava/src/com/google/common/cache/CacheStats.java) via Cache.stats() to access hit rates, eviction counts, and load latencies.

This immutable snapshot provides visibility into cache effectiveness and helps tune maximum size or expiration parameters.

Advanced: Custom Cache Implementations

For scenarios requiring behavior not supported by CacheBuilder, implement the Cache<K,V> interface directly or extend Guava's internal AbstractCache. This approach, defined in guava/src/com/google/common/cache/Cache.java, requires manual implementation of getIfPresent, put, invalidate, and other operations. Most production use cases, however, satisfy requirements through the standard builder and loader extension points.

Summary

  • Extend CacheLoader to define custom loading logic for cache misses, implementing load() and optionally loadAll() or reload().
  • Implement Weigher to control eviction based on entry weight rather than count, using CacheBuilder.weigher() and maximumWeight().
  • Provide RemovalListener to execute callbacks when entries are evicted, expired, or removed explicitly.
  • Override reload() in CacheLoader and enable refreshAfterWrite() for non-blocking asynchronous refresh of stale entries.
  • Call recordStats() to expose CacheStats for monitoring hit rates and performance metrics.
  • Reference source files including CacheBuilder.java, CacheLoader.java, Cache.java, and CacheStats.java to understand the internal wiring of Guava's cache architecture.

Frequently Asked Questions

How do I implement a custom loading strategy for Guava caches?

Extend the abstract CacheLoader class and implement the V load(K key) method with your data retrieval logic. Pass this loader to CacheBuilder.build() to create a LoadingCache that automatically invokes your loading code on cache misses. For batch loading, override loadAll() to optimize bulk retrievals from your data source.

What is the difference between maximumSize and maximumWeight in CacheBuilder?

maximumSize(long size) evicts entries when the cache contains more than the specified number of entries. maximumWeight(long weight) requires a custom Weigher implementation that assigns an integer weight to each entry, allowing eviction based on total resource consumption (such as bytes in memory) rather than entry count.

How can I asynchronously refresh cached values without blocking readers?

Override ListenableFuture<V> reload(K key, V oldValue) in your CacheLoader and configure the cache with refreshAfterWrite(duration, unit). This invokes your reload implementation asynchronously when an entry is requested after the refresh interval has passed, returning the old value immediately while the new value loads in the background.

How do I monitor the performance of a custom Guava cache?

Enable statistics collection by calling recordStats() on your CacheBuilder. After building the cache, call cache.stats() to retrieve a CacheStats object containing hit count, miss count, eviction count, and load latency metrics.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →