# write-through

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

Write-through caching: persist each write before keeping it

<code>WriteThroughCacheFactory</code> supplies read-through caches whose <code>accept</code> 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.** <code>accept</code> calls the writer on the caller's thread, then keeps the value and publishes a <code>Commit</code>, which is an <code>Insert</code>. A later read returns that value without loading. If the writer throws, the failure propagates from <code>accept</code> 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 <code>@PreCondition(IsNotNull.class)</code> and <code>@PostCondition(IsNotNull.class)</code>. 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.** <code>read-through</code> when nothing writes through the cache; <code>write-behind</code> 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

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

```text
Insert sees storage: {answer=42}
Optional[42]
```

A failed write leaves the cache unchanged

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

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

