write-behind

Write-behind caching: run a cache's writes on an injected Executor

WriteBehindCacheFactory decorates each supplied MutableCache so accept submits the write to an injected Executor instead of performing it.

Rationale

Why. A slow write should not hold up the caller.

Key Concepts

Delegated writes

Role. accept submits one task, which writes to the supplied cache. Everything else goes straight to the supplied cache, so a read returns the previous value until the task has run. The caller owns the Executor, which decides when tasks run and in what order.

Pattern. DECORATOR

Failed writes

Role. When a task's write throws, its key is evicted and the failure is rethrown from the task, so the supplied cache no longer answers with a value it could not write.

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).

Thread safety. Safe to share between threads when the supplied cache and the Executor are.

Lane

Purpose. Taking a write off the caller's thread.

Alternative. write-through alone when the caller must know the write persisted before going on.

When to avoid. A write the caller must see succeed: a failure surfaces in the task, never to the caller of accept.

Usage

A write runs when the Executor runs its task; here the Executor is a queue

import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import software.visionary.cache.MutableCache;
import software.visionary.cache.capacity.fixed.FixedSize;
import software.visionary.cache.eviction.recency.LeastRecentlyUsed;
import software.visionary.cache.policy.CachePolicy;
import software.visionary.cache.topology.write.behind.WriteBehindCacheFactory;
import software.visionary.cache.topology.write.through.WriteThroughCacheFactory;

Map<String, String> storage = new ConcurrentHashMap<>(Map.of("answer", "old"));
Supplier<MutableCache<String, String>> writeThrough = new WriteThroughCacheFactory<String, String>(
        key -> Optional.ofNullable(storage.get(key)),
        storage::put,
        new CachePolicy<>(() -> new FixedSize<>(100), LeastRecentlyUsed::new, Instant::now));
Queue<Runnable> tasks = new ArrayDeque<>();
MutableCache<String, String> cache = new WriteBehindCacheFactory<>(writeThrough, tasks::add).get();

cache.accept("answer", "new");
System.out.println(cache.apply("answer").orElseThrow() + ", " + storage.get("answer"));

tasks.remove().run();
System.out.println(cache.apply("answer").orElseThrow() + ", " + storage.get("answer"));

Output

old, old
new, new

A failed write reaches the task, not the caller, and the next read loads again

AtomicInteger loads = new AtomicInteger();
Supplier<MutableCache<String, String>> writeThrough = new WriteThroughCacheFactory<String, String>(
        key -> Optional.of("load " + loads.incrementAndGet()),
        (key, value) -> { throw new IllegalStateException("disk full"); },
        new CachePolicy<>(() -> new FixedSize<>(100), LeastRecentlyUsed::new, Instant::now));
Queue<Runnable> tasks = new ArrayDeque<>();
MutableCache<String, String> cache = new WriteBehindCacheFactory<>(writeThrough, tasks::add).get();
System.out.println(cache.apply("answer").orElseThrow());

cache.accept("answer", "42");
System.out.println("accept returned");

try {
    tasks.remove().run();
} catch (IllegalStateException failed) {
    System.out.println("task threw: " + failed.getMessage());
}

System.out.println(cache.apply("answer").orElseThrow());

Output

load 1
accept returned
task threw: disk full
load 2

Dependencies

  • software.visionary:cache: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.behind.svg
classes.software.visionary.cache.topology.write.behind.svg
packages.svg
packages.svg