Type to search pages and rules.

Browse pages

Outbox Worker

The outbox makes a business commit and its required outgoing message durable in one database transaction.

Markdown39 provisions

Intent#

The outbox makes a business commit and its required outgoing message durable in one database transaction. A Worker dispatches pending records with retry, idempotency, visibility, and safe replay.

Activation#

Activation scope: local.

Applicable specification kinds: use-case, journey.

The consumer enables outbox when a committed change requires durable delivery after process failure. Delivery can carry an integration event, a journey-step command, or a provider side effect.

A use case that stages a durable message selects this extension. The extension activates litespec/backend/architecture#declare-the-execution-host-for-work-that-outlives-a-request. The project declares the execution host that runs the dispatcher.

Baseline relationship#

This extension replaces litespec/backend/persistence#collect-events-without-a-public-unit-of-work.

This extension switches on the Event, Limit, and Runbook record types.

Standards#

Record durable delivery behavior#

ruleguidance

Requirement A durable-delivery specification MUST document delivery guarantee, downstream boundary, duplicate behavior, retry horizon, recovery owner, record owner, and status.

Rationale The specification defines the expected delivery behavior before a message record exists. A draft names its decider and decide-by date.

litespec/ext/outbox#record-durable-delivery-behavior

Exclude manually repeatable reactions#

ruleguidance

Requirement A project MUST NOT activate outbox for a reaction whose delivery guarantee is best-effort and which operators can repeat manually.

Rationale Outbox storage and dispatcher operation are unnecessary when loss is an accepted recoverable outcome. The delivery guarantee comes from the reaction's own declaration. The module specification records it with the vocabulary its event table already uses.

litespec/ext/outbox#exclude-manually-repeatable-reactions

Publish best-effort reactions after commit#

ruleguidance

Requirement A reaction the outbox excludes MUST publish after its commit succeeds without staging an outbox record.

Rationale Best-effort delivery has no durable record. The commit success is its only guarantee.

litespec/ext/outbox#publish-best-effort-reactions-after-commit

Stage messages with business work#

ruleguidance

Requirement A command post-handler MUST stage pending durable messages in the same scoped business session as its aggregate changes.

Rationale One session makes the business commit and outgoing message durable together.

litespec/ext/outbox#stage-messages-with-business-work

Serialize durable messages#

ruleguidance

Requirement An outbox writer MUST serialize each pending durable message into an outbox record.

Rationale A durable record holds the dispatch input after the command scope completes.

litespec/ext/outbox#serialize-durable-messages

Store required message fields#

ruleguidance

Requirement An outbox record MUST contain message identity, kind, stable type, schema version, time, payload, tenant, state, attempt, and next-attempt data.

Rationale Dispatch and recovery need one complete durable description of the message.

litespec/ext/outbox#store-required-message-fields

Store failure and completion evidence#

ruleguidance

Requirement An outbox record MUST retain its last error summary and completion time.

Rationale Operators need failure and completion evidence for backlog review and replay.

litespec/ext/outbox#store-failure-and-completion-evidence

Classify message kinds#

ruleguidance

Requirement An outbox record MUST name its message kind as integration event, journey-step command, or provider side effect, with an integration event citing its Event record.

Rationale The kind decides dispatch ownership and transport. The Event record carries the raises and reacts-to links.

litespec/ext/outbox#classify-message-kinds

Reject independent outbox commits#

ruleguidance

Requirement An outbox writer MUST NOT use another connection or independent commit for required durable delivery.

Rationale A shared PostgreSQL database does not make separate transactions atomic together.

litespec/ext/outbox#reject-independent-outbox-commits

Dispatch claimed records in Worker#

ruleguidance

Requirement A Worker MUST claim pending outbox records, dispatch a bounded batch, and record success or retry information.

Rationale Worker ownership separates durable dispatch from the request transaction.

litespec/ext/outbox#dispatch-claimed-records-in-worker

Exclude request-transaction dispatch#

ruleguidance

Requirement WebApi MUST NOT dispatch a durable message inside the request transaction.

Rationale The request commit only makes the message eligible for later Worker dispatch.

litespec/ext/outbox#exclude-request-transaction-dispatch

Define claim ownership#

ruleguidance

Requirement An outbox claim MUST use a short transaction with the lease owner, lease expiry, and fencing value that litespec/ext/jobs#store-execution-leases defines.

Rationale Claim fields establish which Worker owns one dispatch attempt.

litespec/ext/outbox#define-claim-ownership

Release claim transactions before calls#

ruleguidance

Requirement A Worker MUST NOT hold an outbox claim transaction open during a network call.

Rationale Network delay does not retain database locks or transaction resources.

litespec/ext/outbox#release-claim-transactions-before-calls

Restrict completion to current lease owners#

ruleguidance

Requirement A Worker MUST update an outbox record only while it owns the matching lease and fencing value.

Rationale A stale Worker does not overwrite a newer owner's dispatch result.

litespec/ext/outbox#restrict-completion-to-current-lease-owners

Requeue expired claims#

ruleguidance

Requirement An outbox store MUST return expired claims to eligible work.

Rationale A crashed Worker cannot permanently strand a pending message.

litespec/ext/outbox#requeue-expired-claims

Accept repeated delivery#

ruleguidance

Requirement An outbox consumer MUST handle the same message more than once.

Rationale A crash after external acceptance can cause one message to be delivered again.

litespec/ext/outbox#accept-repeated-delivery

Identify repeated delivery#

ruleguidance

Requirement An outbox consumer MUST use message ID or a business idempotency key at its Command or side-effect boundary.

Rationale One stable identity lets the target suppress a repeated delivery.

litespec/ext/outbox#identify-repeated-delivery

Await target acceptance#

ruleguidance

Requirement A Worker MUST NOT mark an outbox record complete before the target boundary accepts it.

Rationale Completed state claims delivery that has not yet reached the required target.

litespec/ext/outbox#await-target-acceptance

Bound delivery retries#

ruleguidance

Requirement An outbox dispatcher MUST use bounded exponential backoff with jitter.

Rationale Bounded jitter reduces repeated load during a transient target failure.

litespec/ext/outbox#bound-delivery-retries

Classify permanent delivery failure#

ruleguidance

Requirement An outbox dispatcher MUST classify permanent failures, stop automatic retry at its configured limit, and retain safe investigation and replay data.

Rationale A dead-letter outcome preserves evidence after automatic recovery is no longer safe.

litespec/ext/outbox#classify-permanent-delivery-failure

Distinguish dependency outage#

ruleguidance

Requirement A dispatcher MUST distinguish an unavailable outbox store from a failed claimed message.

Rationale Store availability and a claimed record's target result require different recovery paths.

Each has its own signal. The store's availability is a gauge the readiness endpoint reads. A claimed message's outcome is a counter carrying the result, following the OpenTelemetry metrics specification. A single error counter cannot tell an operator whether to restore a database or investigate one destination.

litespec/ext/outbox#distinguish-dependency-outage

Back off unavailable-store polling#

ruleguidance

Requirement A Worker MUST apply bounded backoff and rate-limited logging when its outbox store is unavailable.

Rationale Cold start before schema application and store outage do not justify one exception per loop.

litespec/ext/outbox#back-off-unavailable-store-polling

Restrict message retry to claimed records#

ruleguidance

Requirement A Worker MUST apply message retry and poison handling only to claimed outbox records.

Rationale An unavailable store has no claimed message eligible for delivery retry.

litespec/ext/outbox#restrict-message-retry-to-claimed-records

Gate dispatch on readiness#

ruleguidance

Requirement A Worker host exposing readiness MUST gate dispatch and stop polling stores that cannot serve requests.

Rationale Readiness prevents avoidable work against unavailable storage dependencies.

litespec/ext/outbox#gate-dispatch-on-readiness

Version message types#

ruleguidance

Requirement An outbox message MUST use a stable type name and explicit schema version.

Rationale The pair defines a durable contract for Worker deserialization and dispatch.

litespec/ext/outbox#version-message-types

Support coexisting message producers#

ruleguidance

Requirement A deployed dispatcher MUST process records from every application version that can coexist during rollout or rollback.

Rationale Active deployment versions can place different valid message shapes in one store.

A message record therefore carries its own shape version. The dispatcher selects a reader from that value. Adding an optional member with a defined absent-value behavior needs no new version. Removing a member, renaming one, narrowing a type, or changing a discriminator needs a new version. The older reader stays until no record carries the older version.

litespec/ext/outbox#support-coexisting-message-producers

Publish backlog indicators#

ruleguidance

Requirement An outbox deployment MUST publish pending count, failed count, oldest pending age, attempts, dispatch duration, and success rate.

Rationale These indicators make backlog growth and dispatch quality observable.

litespec/ext/outbox#publish-backlog-indicators

Set delivery alerts#

ruleguidance

Requirement An outbox deployment MUST set alert thresholds as Limit records from its use-case delivery target.

Rationale Alert urgency follows the business consequence of delayed delivery.

litespec/ext/outbox#set-delivery-alerts

Model outbox lifecycle states#

ruleguidance

Requirement An outbox record MUST model its lifecycle as an abstract state base with one sealed record for each of pending, processing, dispatched, and dead-letter.

Rationale pending becomes processing. processing becomes dispatched, pending, or dead-letter. Audited replay moves dead-letter to pending. Three of the four carry data the others do not. processing holds a lease expiry. dispatched holds a dispatch time. dead-letter holds a failure reason and an attempt count. A four-value label forces those onto nullable columns that are meaningful in one state each, which is the shape litespec/backend/domain#model-every-closed-set-of-domain-values-as-a-closed-type-hierarchy removes everywhere else.

Example OutboxState.Processing carries one unexpired lease before a dispatcher records OutboxState.Dispatched or a retry state.

litespec/ext/outbox#model-outbox-lifecycle-states

Preserve message identity during retry#

ruleguidance

Requirement An outbox dispatcher MUST preserve message ID across retry and replay.

Rationale Stable identity lets target idempotency suppress delivery after a crash before dispatch completion.

litespec/ext/outbox#preserve-message-identity-during-retry

Audit outbox replay#

ruleguidance

Requirement An outbox replay MUST record actor, reason, time, and failure without editing the payload: an audit record when audit is selected, a standalone record otherwise.

Rationale Replay history preserves message evidence and explains a renewed dispatch attempt.

litespec/ext/outbox#audit-outbox-replay

Protect active Worker compatibility#

ruleguidance

Requirement New WebApi code MUST emit a message only after every active Worker can read its type and schema version.

Rationale A writer does not create a message shape that a running dispatcher cannot understand.

litespec/ext/outbox#protect-active-worker-compatibility

Deploy readers before writers#

ruleguidance

Requirement A release MUST deploy compatible readers before writers for additive event versions.

Rationale Reader-first deployment preserves dispatch during a mixed-version rollout.

litespec/ext/outbox#deploy-readers-before-writers

Plan undispatched rollback records#

ruleguidance

Requirement A rollback Runbook MUST include records created by the new writer but not yet dispatched.

Rationale These records can outlive the deployment that originally wrote them.

litespec/ext/outbox#plan-undispatched-rollback-records

Conventions#

Group outbox infrastructure#

conventionguidance

Default Keep outbox record, storage, claim, and dispatch infrastructure under Infrastructure/Messaging/Outbox/.

Replacement A consumer records a departure as an Exception record with reason and review date.

Rationale One Infrastructure location contains the persistent dispatch mechanism.

litespec/ext/outbox#group-outbox-infrastructure

Isolate the Worker host#

conventionguidance

Default Keep the outbox Worker host in its own project.

Replacement A consumer records a departure as an Exception record with reason and review date.

Rationale The Worker has independent process lifetime and operating configuration.

litespec/ext/outbox#isolate-the-worker-host

Keep provider mapping with integration#

conventionguidance

Default Keep Integration Event-to-provider mapping in the integration that owns the side effect.

Replacement A consumer records a departure as an Exception record with reason and review date.

Rationale A provider integration owns its transport mapping and external delivery behavior.

litespec/ext/outbox#keep-provider-mapping-with-integration

Dispatch journey-step commands in a new scope#

conventionguidance

Default Dispatch a journey-step command through ICommandMediator in a fresh Worker scope.

Replacement A consumer records a departure as an Exception record with reason and review date.

Rationale The command enters its own application pipeline and transaction.

litespec/ext/outbox#dispatch-journey-step-commands-in-a-new-scope

Avoid automatic durable-bus adoption#

conventionguidance

Default Use project-owned Marten records and Worker dispatch unless a manifest-pinned durable package passes a shared-transaction integration test.

Replacement A consumer records a departure as an Exception record with reason and review date.

Rationale Optional LiteBus durable packages remain unselected until their transaction behavior has evidence.

litespec/ext/outbox#avoid-automatic-durable-bus-adoption

Dependencies#

This extension requires no additional baseline package. Provider-specific dispatch dependencies follow the external integrations extension.

Verification#

IDMarkEvidence
litespec/ext/outbox#record-durable-delivery-behaviorguidanceDoes each delivery specification record guarantee, boundary, retry, owner, and status?
litespec/ext/outbox#exclude-manually-repeatable-reactionsguidanceIs the outbox free of manually repeatable best-effort reactions?
litespec/ext/outbox#publish-best-effort-reactions-after-commitguidanceDo excluded reactions publish after commit without staging records?
litespec/ext/outbox#stage-messages-with-business-workguidanceAre messages staged in the same session as business changes?
litespec/ext/outbox#serialize-durable-messagesguidanceIs each pending message serialized into an outbox record?
litespec/ext/outbox#store-required-message-fieldsguidanceDoes each record hold identity, kind, type, version, time, payload, and state?
litespec/ext/outbox#store-failure-and-completion-evidenceguidanceDoes each record retain its error summary and completion time?
litespec/ext/outbox#classify-message-kindsguidanceDoes each record name its kind and cite its Event record?
litespec/ext/outbox#reject-independent-outbox-commitsguidanceIs writer source free of independent connections and commits?
litespec/ext/outbox#dispatch-claimed-records-in-workerguidanceDoes the Worker claim bounded batches and record outcomes?
litespec/ext/outbox#exclude-request-transaction-dispatchguidanceAre request paths free of durable message dispatch?
litespec/ext/outbox#define-claim-ownershipguidanceDoes each claim use the jobs lease definition in a short transaction?
litespec/ext/outbox#release-claim-transactions-before-callsguidanceAre claim transactions released before network calls?
litespec/ext/outbox#restrict-completion-to-current-lease-ownersguidanceDoes only the current lease owner update records?
litespec/ext/outbox#requeue-expired-claimsguidanceDo expired claims return to eligible work?
litespec/ext/outbox#accept-repeated-deliveryguidanceDoes each consumer handle the same message twice?
litespec/ext/outbox#identify-repeated-deliveryguidanceDoes each consumer suppress repeats by message ID or business key?
litespec/ext/outbox#await-target-acceptanceguidanceDoes the Worker withhold completion until the target accepts?
litespec/ext/outbox#bound-delivery-retriesguidanceAre retries bounded with exponential backoff and jitter?
litespec/ext/outbox#classify-permanent-delivery-failureguidanceDo permanent failures dead-letter with safe replay evidence?
litespec/ext/outbox#distinguish-dependency-outageguidanceIs store outage distinct from claimed-message failure?
litespec/ext/outbox#back-off-unavailable-store-pollingguidanceDoes polling back off with rate-limited logging during outages?
litespec/ext/outbox#restrict-message-retry-to-claimed-recordsguidanceDoes the Worker limit retries to claimed records?
litespec/ext/outbox#gate-dispatch-on-readinessguidanceDoes readiness gate dispatch and stop dead-store polling?
litespec/ext/outbox#version-message-typesguidanceDoes each message carry a stable type name and schema version?
litespec/ext/outbox#support-coexisting-message-producersguidanceDoes the dispatcher read every coexisting producer shape?
litespec/ext/outbox#publish-backlog-indicatorsguidanceDoes the deployment publish pending, failed, age, attempts, duration, and rate?
litespec/ext/outbox#set-delivery-alertsguidanceAre alert thresholds Limit records derived from the delivery target?
litespec/ext/outbox#model-outbox-lifecycle-statesguidanceAre lifecycle states sealed records with documented transitions?
litespec/ext/outbox#preserve-message-identity-during-retryguidanceIs message ID stable across retry and replay?
litespec/ext/outbox#audit-outbox-replayguidanceDoes each replay record actor, reason, time, and prior failure?
litespec/ext/outbox#protect-active-worker-compatibilityguidanceCan every active Worker read new message shapes?
litespec/ext/outbox#deploy-readers-before-writersguidanceDo releases deploy compatible readers before additive writers?
litespec/ext/outbox#plan-undispatched-rollback-recordsguidanceDoes the rollback Runbook cover undispatched new-writer records?
litespec/ext/outbox#group-outbox-infrastructureguidanceIs outbox infrastructure grouped or is an Exception recorded?
litespec/ext/outbox#isolate-the-worker-hostguidanceIs the Worker host separate or is an Exception recorded?
litespec/ext/outbox#keep-provider-mapping-with-integrationguidanceDoes provider mapping stay with its integration or record an Exception?
litespec/ext/outbox#dispatch-journey-step-commands-in-a-new-scopeguidanceAre journey-step commands dispatched in a fresh Worker scope?
litespec/ext/outbox#avoid-automatic-durable-bus-adoptionguidanceIs any durable package pinned with transaction evidence or avoided?