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:
| candidate | reasoning | decision |
|---|---|---|
| treat the event stream as authority | a consumer reads fresh state from the event message itself. | rejected |
| treat the durable record as primary but only after the event arrives | events deliver both facts and signals, eliminating a handoff. | rejected |
| record the durable fact first, emit event as a freshness signal only | supports 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 eventawait stepRuntime.onComplete(() => {queue.send({stepId,status: "completed",output: result,});});// consumer reads the event payload directlyqueue.on("step.completed", (message) => {// mistake: treating message as the factuiStore.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 firstawait stepRuntime.onComplete(async (result) => {const stepRecord = await db.updateStepRecord({stepId,status: "completed",output: result,});// only then emit the signalqueue.send({stepId,eventId: generateIdempotencyKey(), // enables deduplication});});// consumer reads the durable record, uses event as a refresh signalqueue.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.