expiration
Cache expiration: forget a key after a lifetime or an idle spell
Wrap any EvictionStrategy in TimeToLive to remove a key a set time after it was written, or in TimeToIdle to remove it a set time after it was last used.
Rationale
Why. A kept value can go stale long before capacity asks for its room.
Key Concepts
What restarts a deadline
Role. An Insert restarts both. A Hit restarts only TimeToIdle. Nested in either order, the earlier deadline removes the key. Every event and evict also reaches the wrapped strategy.
Pattern. DECORATOR, OBSERVER
How It Works
Guarantees. No public parameter takes null and no public method returns it, declared with contrax's @PreCondition(IsNotNull.class) and @PostCondition(IsNotNull.class).
Lane
Purpose. Forgetting a key because of time rather than room.
Alternative. A capacity and an eviction alone, when a value never goes stale.
When to avoid. A value that must never be read past its deadline: a lookup answers the kept value until the Clockwork has run the removal.
Usage
A Clockwork driven by hand
final class HandClock implements Clockwork {
private record Due(Instant at, Runnable then) { }
private final List<Due> due = new ArrayList<>();
private Instant now = Instant.EPOCH;
@Override public Instant instant() { return now; }
@Override public void after(Duration delay, Runnable then) { due.add(new Due(now.plus(delay), then)); }
void advance(Duration by) {
now = now.plus(by);
List<Due> ready = due.stream().filter(each -> !each.at().isAfter(now)).toList();
due.removeAll(ready);
ready.forEach(each -> each.then().run());
}
}
A 10-second lifetime: the read at second 9 does not extend it, so the read at second 10 loads again
var clock = new HandClock();
var loads = new AtomicInteger();
Cache<String, Integer> cache = new ReadThroughCacheFactory<String, Integer>(
key -> Optional.of(loads.incrementAndGet()),
new CachePolicy<String>(() -> new FixedSize<>(100),
(forget, capacity) -> new TimeToLive<>(new LeastRecentlyUsed<>(forget, capacity),
clock, Duration.ofSeconds(10)),
clock::instant)).get();
System.out.println(cache.apply("a"));
clock.advance(Duration.ofSeconds(9));
System.out.println(cache.apply("a"));
clock.advance(Duration.ofSeconds(1));
System.out.println(cache.apply("a"));
Output
Optional[1]
Optional[1]
Optional[2]A 10-second TimeToIdle instead: reads at seconds 9 and 18 keep the key, then 10 idle seconds remove it
(forget, capacity) -> new TimeToIdle<>(new LeastRecentlyUsed<>(forget, capacity),
clock, Duration.ofSeconds(10))
Reads at seconds 0, 9, 18 and 28
Optional[1]
Optional[1]
Optional[1]
Optional[2]Both: the lifetime ends first at second 10, then idleness at second 16
(forget, capacity) -> new TimeToLive<>(
new TimeToIdle<>(new LeastRecentlyUsed<>(forget, capacity), clock, Duration.ofSeconds(6)),
clock, Duration.ofSeconds(10))
Reads at seconds 0, 5, 10 and 16
Optional[1]
Optional[1]
Optional[2]
Optional[3]Dependencies
software.visionary:cache:2026.10.13at Compilesoftware.visionary:clock:2026.10.13at Compilesoftware.visionary.contrax:processor:2026.10.13at Processorsoftware.visionary:junit-closure:2026.09.17at Testing
Caveats
- The
Clockworkmust read the same clock as the event timestamps: giveCachePolicyclock::instant.
Views
Drawn from the release. A link in a view opens its source at the release tag.