# How to Use Guava's CacheBuilder for In-Memory Caching with Expiration Policies

> Learn to use Guava CacheBuilder for efficient in-memory caching. Implement expiration policies like expireAfterWrite and expireAfterAccess to manage cache data effectively.

- Repository: [Google/guava](https://github.com/google/guava)
- Tags: how-to-guide
- Published: 2026-08-08

---

**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`](https://github.com/google/guava/blob/main/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 in [`CacheBuilder.java`](https://github.com/google/guava/blob/main/CacheBuilder.java).
- **`maximumWeight(long)` with `weigher(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 behind `WeakReference` or `SoftReference` to 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 via `Cache.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:

```java
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:

```java
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`:

```java
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:

```java
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.cache` for constructing immutable, high-performance in-memory caches.
- Use **`expireAfterWrite()`** for fixed entry lifespans and **`expireAfterAccess()`** for idle-time eviction policies.
- **`LoadingCache`** automatically populates missing entries via `CacheLoader` and supports asynchronous refresh via `reload()`.
- Expired entries remain visible to `size()` until maintenance cleanup, but are invisible to `getIfPresent()` and `get()`.
- **`CacheBuilderSpec`** enables string-based configuration parsing, while **`Ticker`** injection facilitates deterministic unit testing.
- All configuration methods are implemented in [`CacheBuilder.java`](https://github.com/google/guava/blob/main/CacheBuilder.java) with 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`](https://github.com/google/guava/blob/main/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.