# expiration

`software.visionary.cache:expiration:2026.10.13` · https://codeberg.org/caching/expiration

Cache expiration: forget a key after a lifetime or an idle spell

Wrap any <code>EvictionStrategy</code> in <code>TimeToLive</code> to remove a key a set time after it was written, or in <code>TimeToIdle</code> to remove it a set time after it was last used.

## Rationale

**Why.** A kept value can go stale long before capacity asks for its room.

## Key Concepts

### What restarts a deadline

**Role.** An <code>Insert</code> restarts both. A <code>Hit</code> restarts only <code>TimeToIdle</code>. Nested in either order, the earlier deadline removes the key. Every event and <code>evict</code> also reaches the wrapped strategy.

**Pattern.** DECORATOR, OBSERVER

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

## Lane

**Purpose.** Forgetting a key because of time rather than room.

**Alternative.** A capacity and an eviction alone, when a value never goes stale.

**When to avoid.** A value that must never be read past its deadline: a lookup answers the kept value until the <code>Clockwork</code> has run the removal.

## Usage

A Clockwork driven by hand

```java
final class HandClock implements Clockwork {
    private record Due(Instant at, Runnable then) { }

    private final List<Due> due = new ArrayList<>();
    private Instant now = Instant.EPOCH;

    @Override public Instant instant() { return now; }
    @Override public void after(Duration delay, Runnable then) { due.add(new Due(now.plus(delay), then)); }

    void advance(Duration by) {
        now = now.plus(by);
        List<Due> ready = due.stream().filter(each -> !each.at().isAfter(now)).toList();
        due.removeAll(ready);
        ready.forEach(each -> each.then().run());
    }
}

```

A 10-second lifetime: the read at second 9 does not extend it, so the read at second 10 loads again

```java
var clock = new HandClock();
var loads = new AtomicInteger();
Cache<String, Integer> cache = new ReadThroughCacheFactory<String, Integer>(
        key -> Optional.of(loads.incrementAndGet()),
        new CachePolicy<String>(() -> new FixedSize<>(100),
                (forget, capacity) -> new TimeToLive<>(new LeastRecentlyUsed<>(forget, capacity),
                        clock, Duration.ofSeconds(10)),
                clock::instant)).get();

System.out.println(cache.apply("a"));
clock.advance(Duration.ofSeconds(9));
System.out.println(cache.apply("a"));
clock.advance(Duration.ofSeconds(1));
System.out.println(cache.apply("a"));

```

Output

```text
Optional[1]
Optional[1]
Optional[2]
```

A 10-second TimeToIdle instead: reads at seconds 9 and 18 keep the key, then 10 idle seconds remove it

```java
(forget, capacity) -> new TimeToIdle<>(new LeastRecentlyUsed<>(forget, capacity),
        clock, Duration.ofSeconds(10))

```

Reads at seconds 0, 9, 18 and 28

```text
Optional[1]
Optional[1]
Optional[1]
Optional[2]
```

Both: the lifetime ends first at second 10, then idleness at second 16

```java
(forget, capacity) -> new TimeToLive<>(
        new TimeToIdle<>(new LeastRecentlyUsed<>(forget, capacity), clock, Duration.ofSeconds(6)),
        clock, Duration.ofSeconds(10))

```

Reads at seconds 0, 5, 10 and 16

```text
Optional[1]
Optional[1]
Optional[2]
Optional[3]
```

## Dependencies

- `software.visionary:cache:2026.10.13` at Compile
- `software.visionary:clock:2026.10.13` at Compile
- `software.visionary.contrax:processor:2026.10.13` at Processor
- `software.visionary:junit-closure:2026.09.17` at Testing

## Caveats

- The <code>Clockwork</code> must read the same clock as the event timestamps: give <code>CachePolicy</code> <code>clock::instant</code>.

