durable event handoff

context

a long-running process reports step changes to a live interface and triggers downstream work. the runtime can retry a step, a socket can reconnect, a consumer can run after the process stopped. a transient message cannot be the source of truth.

why

transient signals and durable facts fail differently; only durable facts fail safely. an event can be delayed, duplicated, or lost after the work succeeded — if the event is the source of truth, its loss becomes the business's loss. making the record authority bounds every signal failure to freshness: worst case is a stale view the next read repairs, not a correctness loss.

invariants

  • the durable record is authority; the event is only a freshness signal.
  • event handling is idempotent: the same event twice produces one projection refresh and one downstream action.
  • a consumer never advances business state from event payload alone; it reloads the authoritative record.
  • recording failure blocks the business result; publication failure does not.

design

one boundary records the durable step fact under a scoped idempotency identity, then emits a structured event carrying only enough data to locate the fact and invalidate its projection. consumers reload authoritative state before rendering or advancing. recording and publication have separate failure handling: the durable write guards business truth; a publication failure becomes a typed, observable recovery concern, never a fabricated completion.

decision process

the critical choice: what is the source of truth. three candidates:

candidatereasoningdecision
treat the event stream as authoritya consumer reads fresh state from the event message itself.rejected
treat the durable record as primary but only after the event arrivesevents deliver both facts and signals, eliminating a handoff.rejected
record the durable fact first, emit event as a freshness signal onlysupports idempotent retry, later audit, complete reload; event delays or duplicates degrade freshness, not truth.selected

an event can arrive late, be resent, or be lost while the write already succeeded. if the event is authority, a missing event means no one knows the step completed. if the record is authority, the same event arriving twice still yields one coherent state. the event stream is a freshness mechanism only: fact first, signal second.

failure modes prevented

  • a retried step emitting a second event cannot double-count progress — consumers converge on the keyed durable fact.
  • a dropped event degrades freshness, not correctness: the next read or reconnect rebuilds state from the record.
  • a publication failure surfaces as a typed recovery concern instead of a fabricated completion event or a silently stale interface.

verification reasoning

force three cases: duplicate event, missing event, restarted consumer. in all three, the interface converges by reloading the durable record. then force event publication failure and prove the record stays correct while the system emits a recovery diagnostic.

good and bad

bad

// step completes, emits event
await stepRuntime.onComplete(() => {
queue.send({
stepId,
status: "completed",
output: result,
});
});
// consumer reads the event payload directly
queue.on("step.completed", (message) => {
// mistake: treating message as the fact
uiStore.setStepOutput(message.output);
uiStore.markStepComplete(message.stepId);
});

the event is the only record. delayed or retransmitted → the consumer misses the completion (stale UI) or double-counts it (incorrect metrics). lost → no one ever knows the step ran successfully. the UI and the durable record can permanently diverge.

good

// step completes, writes durable record first
await stepRuntime.onComplete(async (result) => {
const stepRecord = await db.updateStepRecord({
stepId,
status: "completed",
output: result,
});
// only then emit the signal
queue.send({
stepId,
eventId: generateIdempotencyKey(), // enables deduplication
});
});
// consumer reads the durable record, uses event as a refresh signal
queue.on("step.completed", async (message) => {
// event tells the consumer "something changed"
const freshRecord = await api.getStepRecord(message.stepId);
uiStore.setStepOutput(freshRecord.output);
uiStore.markStepComplete(freshRecord.status);
});

the durable record is the fact; the event is only a "refresh now" signal. delayed, lost, or duplicated — the consumer's next read of the authoritative record still yields the correct state. an event arriving after a crash triggers a reload of the already-persisted record.

search pages

go to any page