# write-behind

`software.visionary.cache:write-behind:2026.10.13` · https://codeberg.org/caching/write-behind

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

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

## Rationale

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

## Key Concepts

### Delegated writes

**Role.** <code>accept</code> 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 <code>Executor</code>, 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 <code>@PreCondition(IsNotNull.class)</code> and <code>@PostCondition(IsNotNull.class)</code>.

**Thread safety.** Safe to share between threads when the supplied cache and the <code>Executor</code> are.

## Lane

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

**Alternative.** <code>write-through</code> 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 <code>accept</code>.

## Usage

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

```java
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

```text
old, old
new, new
```

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

```java
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

```text
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

