OnceOnly
The OnceOnly attribute marks a reactor handler that must not run again when its observer is replayed.
There are scenarios where you need side effects to occur only on the initial event processing, such as sending notifications, triggering external integrations, or performing non-idempotent operations.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reactors;
[EventType]public record OnceOnlyOrderPlaced(string OrderId);
public class OnceOnlyOrderReactor : IReactor{ [OnceOnly] public void SendNotification(OnceOnlyOrderPlaced @event) { // This code will only execute once when the event is first processed, // and will be skipped during replay. }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.OnceOnlyimport io.cratis.chronicle.observation.Reactor
@EventType(id = "once-only-order-placed")data class OnceOnlyOrderPlaced(val orderId: String)
@Reactorclass OnceOnlyOrderReactor { @OnceOnly fun sendNotification(event: OnceOnlyOrderPlaced) { // Runs once when the event is first observed, and is skipped during replay. }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.OnceOnly;import io.cratis.chronicle.observation.Reactor;
@EventType(id = "once-only-order-placed")record OnceOnlyOrderPlaced(String orderId) {}
@Reactorclass OnceOnlyOrderReactor { @OnceOnly void sendNotification(OnceOnlyOrderPlaced event) { // Runs once when the event is first observed, and is skipped during replay. }}OnceOnly gives you a clean way to separate one-time operations from normal reactor behavior.
What it does and does not guarantee
Section titled “What it does and does not guarantee”OnceOnly takes the handler out of replay — the deliberate re-delivery you trigger by rewinding an
observer, or that a redaction or revision triggers for you. That is the whole of it.
It is not an exactly-once delivery guarantee. In particular, recovering a failed partition re-delivers the
event as an ordinary observation, so a OnceOnly handler runs again — which is the point of a retry: the
handler failed before its side effect ran, and recovery exists to run it. Keep designing for the handler
being invoked more than once when it fails partway through.
Where you put it
Section titled “Where you put it”OnceOnly goes on a handler method or on the reactor class, and the two mean different things:
- On a method — that handler is skipped for every event that arrives as part of a replay. The rest of the reactor still replays, which is what you want when only one of its handlers has a non-idempotent side effect.
- On the class — the whole reactor registers as non-replayable, so a replay of its observer never starts and none of its handlers run again. This is strictly coarser: it takes every handler out of replay, not just the one with the side effect.
Reach for the method placement unless the entire reactor exists to do something that must not happen twice.