🗓️ 06022026 1116

CAFFEINE-REFRESH-VS-EXPIRE

Overview

FeatureexpireAfterWriterefreshAfterWrite
BehaviorEntry is removed after durationEntry is refreshed asynchronously after duration
Stale readsNo (returns null or recomputes synchronously)Yes (returns stale value while refreshing)
BlockingYes (on cache miss)No (refresh happens in background)
Use caseData that must not be staleData where eventual consistency is acceptable

expireAfterWrite

Entries are evicted after the specified duration. The next request will block while the value is recomputed.

LoadingCache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> userService.fetchUser(key));

// After 5 minutes, entry is removed
// Next get() blocks until fetchUser() completes
User user = cache.get("user123");

Timeline:

t=0    -> cache.get("A") -> MISS, blocks, fetches, returns fresh value
t=4min -> cache.get("A") -> HIT, returns cached value
t=6min -> cache.get("A") -> MISS (expired), blocks, fetches, returns fresh value

refreshAfterWrite

Entries are refreshed asynchronously after the specified duration. The stale value is returned immediately while refresh happens in the background.

LoadingCache<String, User> cache = Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.expireAfterWrite(10, TimeUnit.MINUTES) // recommended: set expiry as upper bound
.build(key -> userService.fetchUser(key));

// After 5 minutes, entry is marked for refresh
// get() returns stale value immediately, triggers async refresh
User user = cache.get("user123");

Timeline:

t=0    -> cache.get("A") -> MISS, blocks, fetches, returns fresh value
t=4min -> cache.get("A") -> HIT, returns cached value
t=6min -> cache.get("A") -> HIT (stale), returns cached value, triggers async refresh
t=6min+1ms -> refresh completes in background, cache updated
t=7min -> cache.get("A") -> HIT, returns fresh value

Key Differences

1. Blocking Behavior

// expireAfterWrite: blocks on expired entry
cache.get("key"); // blocks if expired

// refreshAfterWrite: never blocks (returns stale)
cache.get("key"); // returns immediately, even if stale

Always pair refreshAfterWrite with expireAfterWrite as a failsafe:

Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // refresh after 1 min
.expireAfterWrite(5, TimeUnit.MINUTES) // hard expiry after 5 min
.build(loader);

Why is expireAfterWrite needed?

refreshAfterWrite only triggers on access. The expiry acts as a safety net for edge cases:

ScenarioWithout expireAfterWriteWith expireAfterWrite
Key never accessed againStale data sits in cache foreverEvicted after 5 min
Refresh throws exceptionStale value remains indefinitelyEvicted after 5 min
Refresh keeps failingData becomes arbitrarily oldHard limit on staleness

In normal operation (frequent reads, successful refreshes), expireAfterWrite rarely triggers — the refresh keeps the entry fresh. It's a worst-case bound, not for typical flow.

When to Use What

ScenarioRecommendation
User session dataexpireAfterWrite (security-sensitive)
Config/settingsrefreshAfterWrite + expireAfterWrite
External API responsesrefreshAfterWrite + expireAfterWrite
Rate limit countersexpireAfterWrite (must be accurate)
Feature flagsrefreshAfterWrite (eventual consistency OK)

References