write-through
Write-through caching: persist each write before keeping it
WriteThroughCacheFactory supplies read-through caches whose accept persists to backing storage first and keeps the value only if that succeeds.
Rationale
Why. A cache in front of storage is only right while the two agree.
Key Concepts
Write-through persistence
Role. accept calls the writer on the caller's thread, then keeps the value and publishes a Commit, which is an Insert. A later read returns that value without loading. If the writer throws, the failure propagates from accept and the cache keeps what it held.
Pattern. DECORATOR
Loads beside writes
Role. A missing key loads as in a read-through cache. A load begun before a successful write cannot replace the written value.
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). Writes to one key run one at a time.
Thread safety. A cache may be shared between threads. Loaders, writers and OBSERVERs must support concurrent calls.
Lane
Purpose. Writing through a cache to backing storage.
Alternative. read-through when nothing writes through the cache; write-behind to take the write off the caller's thread.
When to avoid. Storage that other writers change: a kept value stays until it is evicted.
Usage
A write reaches storage before the cache keeps it
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import software.visionary.cache.MutableCache;
import software.visionary.cache.capacity.fixed.FixedSize;
import software.visionary.cache.events.Insert;
import software.visionary.cache.eviction.recency.LeastRecentlyUsed;
import software.visionary.cache.policy.CachePolicy;
import software.visionary.cache.topology.write.through.WriteThroughCacheFactory;
Map<String, String> storage = new ConcurrentHashMap<>();
MutableCache<String, String> cache = new WriteThroughCacheFactory<String, String>(
key -> Optional.ofNullable(storage.get(key)),
storage::put,
new CachePolicy<>(() -> new FixedSize<>(100), LeastRecentlyUsed::new, Instant::now)).get();
cache.add(event -> {
if (event instanceof Insert<String, ?>) {
System.out.println("Insert sees storage: " + storage);
}
});
cache.accept("answer", "42");
storage.clear();
System.out.println(cache.apply("answer"));
Output
Insert sees storage: {answer=42}
Optional[42]A failed write leaves the cache unchanged
MutableCache<String, String> cache = new WriteThroughCacheFactory<String, String>(
key -> Optional.of("stored"),
(key, value) -> { throw new IllegalStateException("disk full"); },
new CachePolicy<>(() -> new FixedSize<>(100), LeastRecentlyUsed::new, Instant::now)).get();
System.out.println(cache.apply("answer"));
try {
cache.accept("answer", "42");
} catch (IllegalStateException failed) {
System.out.println("accept threw: " + failed.getMessage());
}
System.out.println(cache.apply("answer"));
Output
Optional[stored]
accept threw: disk full
Optional[stored]Dependencies
software.visionary.cache:read-through:2026.10.13at Compilesoftware.visionary.cache:write-behind:2026.10.13at Compilesoftware.visionary.contrax:processor:2026.10.13at Processorsoftware.visionary:junit-closure:2026.09.17at Testing
Views
Drawn from the release. A link in a view opens its source at the release tag.