How to Use Guava's CacheBuilder for In-Memory Caching with Expiration Policies
Guava's CacheBuilder class provides a fluent API for constructing high-performance in-memory caches with configurable expiration policies, supporting both fixed-duration lifespans via expireAfterWrite() and idle-time eviction via expireAfterAccess().
Guava's caching utilities, maintained in the google/guava repository, deliver a production-ready solution for JVM-based applications requiring sophisticated in-memory caching without external dependencies. The CacheBuilder API enables developers to compose immutable Cache<K,V> or LoadingCache<K,V> instances with granular control over size limits, time-based eviction, reference strength, and statistics collection.
Core Architecture of CacheBuilder
The CacheBuilder class, located in guava/src/com/google/common/cache/CacheBuilder.java, implements a fluent-interface pattern that accumulates configuration settings to build immutable cache instances. When features like expiration or size limits are enabled, Guava injects periodic maintenance logic into the LocalCache implementation, which runs during write operations, occasional reads, or explicit Cache.cleanUp() calls.
Key Configuration Methods
The builder provides type-safe methods for defining eviction and expiration behavior:
maximumSize(long): Caps the entry count and evicts LRU entries when exceeded. Defined at lines 498-510 inCacheBuilder.java.maximumWeight(long)withweigher(Weigher): Sets a custom weight-based limit using a user-provided weigher function (lines 540-553).expireAfterWrite(Duration): Removes entries a fixed duration after creation or last update (lines 735-752).expireAfterAccess(Duration): Removes entries after a fixed duration of inactivity (lines 808-825).weakValues()/softValues(): Stores values behindWeakReferenceorSoftReferenceto allow GC reclamation (lines 666-670).removalListener(RemovalListener): Registers callbacks for eviction, expiration, or garbage collection events (lines 889-898).recordStats(): Enables hit/miss statistics accessible viaCache.stats()(lines 1016-1020).ticker(Ticker): Accepts a custom time source for deterministic testing (lines 555-560).
Understanding Expiration Policies
Guava supports two distinct time-based expiration strategies that determine when entries become invisible to lookups and eligible for removal.
Expire After Write
The expireAfterWrite(Duration) method (or the deprecated expireAfterWrite(long, TimeUnit) overload) measures entry lifetime from the moment of creation or value replacement. After the configured duration elapses, the entry becomes inaccessible via getIfPresent() and is eventually purged during maintenance cycles. Internally, the builder stores this duration in nanoseconds as expireAfterWriteNanos.
Supplying a duration of zero effectively disables caching by treating the cache as maximumSize(0), which is useful for testing configurations.
Expire After Access
The expireAfterAccess(Duration) method extends entry lifetime based on the last read or write operation. Each access resets the timestamp, making this policy ideal for "idle-time" caches where frequently accessed data should persist while dormant entries expire. This differs from expireAfterWrite because read operations extend the lifespan.
Important: Expired entries remain counted by Cache.size() until the maintenance thread removes them, though they are never returned by lookup operations.
Practical Implementation Examples
Basic Cache with Fixed Expiration
Create a simple cache that evicts entries five minutes after insertion:
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.time.Duration;
Cache<String, Integer> cache = CacheBuilder.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofMinutes(5)) // evict 5 min after write
.build();
cache.put("answer", 42);
Integer value = cache.getIfPresent("answer"); // → 42
LoadingCache with Automatic Refresh and Idle Timeout
For caches that automatically load missing entries, use LoadingCache with CacheLoader and combine expiration with background refresh:
import com.google.common.cache.*;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.time.Duration;
LoadingCache<String, String> userCache = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofHours(1)) // idle-time eviction
.refreshAfterWrite(Duration.ofMinutes(10)) // refresh stale entries
.recordStats() // enable statistics
.build(new CacheLoader<>() {
@Override
public String load(String key) throws Exception {
return fetchUserFromDatabase(key); // expensive operation
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) {
// Asynchronous refresh returns a future that updates the entry
return Futures.immediateFuture(fetchUserFromDatabase(key));
}
});
String userInfo = userCache.get("bob"); // loads on miss, refreshes after 10 min
CacheStats stats = userCache.stats(); // hit/miss statistics
Monitoring Evictions with Removal Listeners
Track why entries leave the cache using a RemovalListener:
import com.google.common.cache.*;
RemovalListener<String, String> listener = notification -> {
System.out.printf("Evicted key=%s, cause=%s%n",
notification.getKey(), notification.getCause());
};
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(Duration.ofMinutes(30))
.removalListener(listener)
.build();
cache.put("temp", "value");
// When the entry expires or is evicted, the listener receives the notification
Unit Testing with a Fake Ticker
For deterministic testing of expiration logic, inject a custom Ticker that you control manually:
import com.google.common.base.Ticker;
import com.google.common.cache.*;
import java.util.concurrent.TimeUnit;
class TestTicker extends Ticker {
private long nanos = 0;
@Override
public long read() { return nanos; }
void advance(long amount, TimeUnit unit) {
nanos += unit.toNanos(amount);
}
}
TestTicker ticker = new TestTicker();
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(Duration.ofSeconds(10))
.ticker(ticker) // inject the fake time source
.build();
cache.put("k", "v");
ticker.advance(11, TimeUnit.SECONDS); // move time forward
assert cache.getIfPresent("k") == null; // entry has expired
Summary
- Guava CacheBuilder provides a fluent API in
com.google.common.cachefor constructing immutable, high-performance in-memory caches. - Use
expireAfterWrite()for fixed entry lifespans andexpireAfterAccess()for idle-time eviction policies. LoadingCacheautomatically populates missing entries viaCacheLoaderand supports asynchronous refresh viareload().- Expired entries remain visible to
size()until maintenance cleanup, but are invisible togetIfPresent()andget(). CacheBuilderSpecenables string-based configuration parsing, whileTickerinjection facilitates deterministic unit testing.- All configuration methods are implemented in
CacheBuilder.javawith validation and nanosecond precision timing.
Frequently Asked Questions
What is the difference between expireAfterWrite and expireAfterAccess in Guava CacheBuilder?
expireAfterWrite measures time from the entry's creation or last value update, ensuring data never exceeds a fixed age regardless of access patterns. expireAfterAccess measures time from the last read or write operation, resetting the timer on each access to keep frequently used data alive while evicting idle entries.
How do I test expiration logic without waiting for real time?
Inject a custom Ticker implementation via CacheBuilder.ticker() to control the time source manually. As shown in CacheBuilder.java lines 555-560, this allows you to advance time programmatically in unit tests to verify expiration behavior without Thread.sleep() calls.
What happens when I set expireAfterWrite to zero?
When you provide a zero duration to expireAfterWrite(), Guava treats this as a size-zero cache equivalent to maximumSize(0), effectively disabling caching and ensuring every get() results in a miss or cache load. This serves as a convenient configuration toggle for disabling caching in test environments.
How does Guava handle expired entries during cache maintenance?
Expired entries are logically removed immediately (invisible to lookups) but physically removed during periodic maintenance cycles triggered by write operations, occasional read operations, or explicit Cache.cleanUp() calls. Until physical removal, expired entries continue to count toward Cache.size() totals as implemented in the LocalCache class.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →