Skip to content

Idempotency contract

Every handler for a kind listed in subscribed_event_kinds runs under an at-least-once delivery contract. That means the same event may be delivered twice with the same event_id, and the second delivery must leave post-state unchanged.

The kernel enforces that contract in two ways:

  1. A startup validator fails closed when a subscribed handler declares neither @idempotent nor replay_key(event).
  2. The replay harness fires the same event twice with the same event_id and asserts that state after the first delivery equals state after the second.

Use one of these patterns on every subscribed handler:

  • @idempotent: true when replay is already safe because the handler does an upsert or checks event_id before mutating state.
  • replay_key(event) when the handler needs an explicit, deterministic dedup key.
export const onRecoveryRequested = {
  '@idempotent': true,
  async handle(event, state) {
    state.processed_event_ids ??= [];
    if (!state.processed_event_ids.includes(event.event_id)) {
      state.processed_event_ids.push(event.event_id);
    }
  },
};

This is the simplest way to think about replay safety:

export const onCommandReceived = {
  '@idempotent': true,
  async handle(_event, state) {
    state.command_count = (state.command_count ?? 0) + 1;
  },
};

First delivery:

{ "command_count": 1 }

Second delivery of the same event:

{ "command_count": 2 }

That handler is not idempotent. Replay changed state.

export const onCommandReceived = {
  '@idempotent': true,
  async handle(event, state) {
    state.processed_event_ids ??= [];
    if (!state.processed_event_ids.includes(event.event_id)) {
      state.processed_event_ids.push(event.event_id);
    }
  },
};

First delivery:

{ "processed_event_ids": ["evt-123"] }

Second delivery of the same event:

{ "processed_event_ids": ["evt-123"] }

State is unchanged, so replay is safe.

For each subscribed handler, the harness:

  1. Builds a fresh handler state.
  2. Delivers one event.
  3. Captures post-state.
  4. Delivers the same event again with the same event_id.
  5. Captures post-state again.
  6. Fails if the snapshots differ.

The harness is intentionally simple. If a handler cannot survive this replay, it is not safe for the at-least-once delivery contract.

  • Dedup by event_id.
  • Prefer upsert or set semantics over increment semantics.
  • Keep the dedup write and the effect in the same transaction boundary.
  • Declare @idempotent or replay_key(event) before wiring the handler into subscribed_event_kinds.