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.13 at Compile
  • software.visionary.cache:write-behind:2026.10.13 at Compile
  • software.visionary.contrax:processor:2026.10.13 at Processor
  • software.visionary:junit-closure:2026.09.17 at Testing

Views

Drawn from the release. A link in a view opens its source at the release tag.

architecture.svg
architecture.svg
classes.software.visionary.cache.topology.write.through.svg
classes.software.visionary.cache.topology.write.through.svg
packages.svg
packages.svg