Table of Contents

Transactional outbox/inbox for durable publishing

Context and Problem Statement

The Queue API must both record a request (write RequestRecord) and publish a message (ISubmitRequest). If these are two independent operations, a crash between them leaves the audit trail and the queue inconsistent — a request marked Queued that was never published, or vice versa. How do we make the state change and the publish atomic?

Decision Drivers

  • Exactly the right messages get published — no lost or phantom messages.
  • The audit trail and the queue can never disagree.
  • Consumers can tolerate redelivery without double-processing.

Considered Options

  • EF Core transactional outbox + inbox (AddEntityFrameworkOutbox + UseBusOutbox)
  • Publish directly inside the request handler, best-effort
  • Two-phase commit / distributed transaction across DB and broker

Decision Outcome

Chosen option: EF Core transactional outbox + inbox, because the publish is written to an outbox table inside the same DB transaction as the business state change, then dispatched to the transport by a background delivery service. The inbox deduplicates on the consumer side.

Consequences

  • Good, because state change and publish are atomic — the dual-write problem is eliminated.
  • Good, because the inbox gives consumer-side dedup, supporting idempotent processing.
  • Good, because it needs no distributed transaction coordinator.
  • Bad, because there is a small delivery latency (the outbox is swept asynchronously).
  • Bad, because the outbox/inbox tables must be included in migrations and maintained.

Confirmation

The InitialCreate migration includes InboxState, OutboxState, and OutboxMessage. Consumer idempotency under redelivery is covered by Redelivery_of_same_correlation_id_stays_completed.

More Information

Side effects inside handlers must still be idempotent (keyed on CorrelationId); the outbox guarantees delivery semantics, not handler-side effect idempotence.