cache
Cache: a Function that keeps what it answered, managed by an eviction STRATEGY
Contracts for a cache: a Cache<KEY, VALUE> is a Function from key to Optional<VALUE> with its own Capacity and EvictionStrategy, publishing CacheEvents to its OBSERVERs. This module holds the contracts, not a cache.
Rationale
Why. Code that reads, bounds or evicts from a cache should not depend on which cache it is given.
Key Concepts
Cache
Role. apply(key) answers the value when present, otherwise empty. capacity() and strategy() return the STRATEGYs that bound and evict this cache. MutableCache adds accept(key, value).
Pattern. STRATEGY
Lifecycle events
Role. Sealed CacheEvent permits Hit, Miss, Insert and Evict, so a switch over them is exhaustive. A ValueInsertion is an Insert that carries the value, and a cache may publish its own.
Pattern. OBSERVER
Capacity and eviction
Role. Each observes a cache's events. Capacity.exceeded() says whether its bound requires eviction, independently of what is chosen for removal. EvictionStrategy.evict(key) removes an entry; an absent key has no effect.
Pattern. STRATEGY, 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. The types a cache, its capacity and its eviction are written against.
Alternative. java.util.Map when nothing is ever forgotten.
When to avoid. A value that must not be lost: a cache may forget anything it keeps.
Usage
React to a lifecycle event
import java.time.Instant;
import software.visionary.cache.events.CacheEvent;
import software.visionary.cache.events.Evict;
import software.visionary.cache.events.Hit;
import software.visionary.cache.events.Insert;
import software.visionary.cache.events.Miss;
import software.visionary.cache.events.ValueInsertion;
String described(CacheEvent<String, ?> event) {
return switch (event) {
case Hit<String, ?> hit -> "hit " + hit.key();
case Miss<String, ?> miss -> "miss " + miss.key();
case ValueInsertion<String, ?> kept -> "insert " + kept.key() + "=" + kept.value();
case Insert<String, ?> keyOnly -> "insert " + keyOnly.key();
case Evict<String, ?> evict -> "evict " + evict.key();
};
}
Instant now = Instant.now();
System.out.println(described(new Miss.Occurred<>("answer", now)));
System.out.println(described(new Insert.Value<>("answer", 42, now)));
System.out.println(described(new Evict.Occurred<>("answer", now)));
Output
miss answer
insert answer=42
evict answer
A capacity of your own
final class TotalKeyLength implements Capacity<String> {
private final int limit;
private final Set<String> kept = new HashSet<>();
TotalKeyLength(int limit) {
this.limit = limit;
}
@Override public boolean exceeded() {
return kept.stream().mapToInt(String::length).sum() > limit;
}
@Override public void update(CacheEvent<String, ?> event) {
if (event instanceof Insert<String, ?>) {
kept.add(event.key());
} else if (event instanceof Evict<String, ?>) {
kept.remove(event.key());
}
}
}
Instant now = Instant.now();
Capacity<String> capacity = new TotalKeyLength(9);
capacity.update(new Insert.Key<>("four", now));
capacity.update(new Insert.Key<>("five", now));
System.out.println(capacity.exceeded());
capacity.update(new Insert.Key<>("three", now));
System.out.println(capacity.exceeded());
capacity.update(new Evict.Occurred<>("four", now));
System.out.println(capacity.exceeded());
Output
false
true
falseDependencies
software.visionary:events:2026.10.13at Compilesoftware.visionary.contrax:processor:2026.10.13at Processor
Views
Drawn from the release. A link in a view opens its source at the release tag.