LiteBus
Reliable messaging

Outbox

The outbox stores messages for publication after a state change commits, so a message is never published for a change that rolled back and never lost for a change that committed. This page is the contract and registration reference for the neutral outbox core. Read Reliable Messaging first.

If your command handler must enqueue events in the same database transaction as domain persistence, read Transactional messaging writes before choosing between IOutbox and ITransactionalOutbox.

Core Module

LiteBus.Outbox is transport-neutral orchestration only. It registers:

ServiceImplementationRole
IOutboxOutboxEnqueue and serialize messages into storage
IOutboxProcessorPipelinedOutboxProcessorLease due messages, dispatch, record retry or dead-letter state
OutboxProcessorOptionsoptions instanceBatch size, lease duration, retry policy

Storage and dispatch register inside AddOutbox through Use* extensions.

Contract

Use IOutbox.EnqueueAsync when a message must survive process failure and be published after the surrounding state change commits.

public sealed record OrderSubmittedIntegrationEvent
{
    public required Guid OrderId { get; init; }
}

var receipt = await outbox.EnqueueAsync(
    OutboxEnqueueItem<OrderSubmittedIntegrationEvent>.From(
        new OrderSubmittedIntegrationEvent { OrderId = orderId },
        OutboxEnqueueMetadata.Immediate with
        {
            Identity = new MessageIdentity.Supplied(messageId),
            Target = new PublicationTarget.Topic("orders"),
            Trace = new MessageTrace.Correlated(correlationId),
        }),
    cancellationToken);

receipt.Outcome is Enqueued when the store inserted a row and AlreadyEnqueued when a duplicate message ID or tenant-scoped idempotency key resolved to an existing row. The receipt confirms storage, not broker publication.

Register each stored message type in IMessageContractRegistry with a stable name and version, or apply [MessageContract] and call RegisterFromAssembly during module configuration.

Registration (Nested Builder Only)

Register contracts, storage, dispatch, and the processor inside one AddOutbox call:

services.AddLiteBus(builder =>
{
    builder.AddOutbox(outbox =>
    {
        outbox.Contracts.Register<OrderSubmittedIntegrationEvent>(
            name: "orders.events.order-submitted",
            version: 1);

        outbox.UseProcessorOptions(new OutboxProcessorOptions { BatchSize = 100 });

        outbox.UseInMemoryStorage();
        outbox.UseInProcessDispatch();
        outbox.EnableOutboxProcessor(host => host.PollInterval = TimeSpan.FromSeconds(1));
    });
});

Processing Versus Publishing

Inbox and outbox use asymmetric naming on purpose. Inbox acceptance (AcceptAsync) only stores a command; processing happens later in PipelinedInboxProcessor. Outbox enqueue (EnqueueAsync) only stores an event; publication happens later in PipelinedOutboxProcessor when a dispatcher calls IEventMediator or an external broker. Do not call PublishAsync directly for cross-process notifications that must commit with application state.

Dispatch (Use* Extensions)

ExtensionPackageBehavior
UseInProcessDispatch()LiteBus.Outbox.Dispatch.InProcessDeserialize and IEventMediator.PublishAsync
UseAmqpDispatch(...)LiteBus.Outbox.Dispatch.AmqpPublish through the root AMQP transport with contract headers
UseAzureServiceBusDispatch(...)LiteBus.Outbox.Dispatch.AzureServiceBusPublish through the root Azure Service Bus transport
UseAwsSqsDispatch(...)LiteBus.Outbox.Dispatch.AwsSqsPublish through the root Amazon SQS transport
UseKafkaDispatch(...)LiteBus.Outbox.Dispatch.KafkaPublish through the root Kafka transport
UseInMemoryDispatch(...)LiteBus.Outbox.Dispatch.InMemoryPublish through in-memory transport (tests, local pipelines)

Register exactly one dispatcher per outbox module.

Storage (Use* Extensions)

ExtensionPackage
UseInMemoryStorage()LiteBus.Outbox.Storage.InMemory
UsePostgreSqlStorage(...)LiteBus.Outbox.Storage.PostgreSql
UseEntityFrameworkCoreStorage(...)LiteBus.Outbox.Storage.EntityFrameworkCore

In-Memory Store

Process-local store for unit tests and local development. Thread-safe within one process. Register a dispatcher before EnableOutboxProcessor().

PostgreSQL Store

builder.AddAmqpTransport(new AmqpConnectionOptions
{
    Uri = new Uri(configuration.GetConnectionString("Amqp")!)
});

builder.AddOutbox(outbox =>
{
    outbox.Contracts.Register<OrderSubmittedIntegrationEvent>(
        "orders.events.order-submitted",
        version: 1);

    outbox.UsePostgreSqlStorage(pg =>
    {
        pg.UseDataSource(dataSource);
        pg.EnsureSchemaCreationOnStartup(); // development only
    });

    outbox.UseAmqpDispatch(o => o.DefaultDestination = "orders.events");

    outbox.EnableOutboxProcessor();
});

PostgreSQL outbox schema version 3 stores opaque payload text and adds lease_generation. Apply the v2 and v3 SQL files to an existing v6 table before startup. See PostgreSQL Schema Management.

Pass the same NpgsqlDataSource instance to inbox and outbox when both use one database. See Transactional messaging writes.

Transactional Writes (Domain + Outbox)

Default IOutbox.EnqueueAsync commits immediately through the singleton store. It does not join an open domain transaction.

SituationAPI
Ingress, tools, fire-and-forget enqueueIOutbox
Command handler + EF DbContextITransactionalOutbox<TContext>: Outbox EF Core storage
Command handler + PostgreSQL (Marten, Dapper, ADO.NET)ITransactionalOutbox: Transactional messaging writes

EF registration, interceptor setup, and duplicate-idempotency behavior on the transactional path are documented in Outbox Entity Framework Core Storage.

Delivery Semantics

Outbox publication is at-least-once. PipelinedOutboxProcessor dispatches through IOutboxDispatcher before it persists terminal published state. A crash or PersistAsync failure after a successful broker publish leaves the row leased or pending and the processor retries, which can duplicate publication downstream. Consumers must deduplicate or handle retries idempotently (for example with MessageId, IdempotencyKey, or broker deduplication headers).

LiteBus does not implement a two-phase publish acknowledgment (broker ack token persisted before terminal state) in v6. Treat external side effects as idempotent, or wrap dispatch in a custom IOutboxDispatcher that records an outbox-side ack before returning success.

The integration test ProcessPendingAsync_WhenPersistSkippedAfterPublish_ShouldRepublishOnRetry in LiteBus.Outbox.Dispatch.InMemory.IntegrationTests demonstrates the crash window: a simulated persist skip after transport publish causes a second publish when the lease is reclaimed.

HonorShutdownTokenOnPersist on OutboxProcessorOptions mirrors the inbox option (default false uses CancellationToken.None on terminal persist).

Processing Flow

  1. Application code enqueues through IOutbox.EnqueueAsync or ITransactionalOutbox.EnqueueAsync (when domain and outbox must share one transaction).
  2. The writer resolves contract name and version.
  3. The writer serializes and stores an outbox envelope.
  4. PipelinedOutboxProcessor leases due messages.
  5. The processor calls IOutboxDispatcher.DispatchAsync for each message.
  6. The processor marks the message published, failed for retry, or dead-lettered.

Store Roles

InterfaceUsed byResponsibility
IOutboxStoreIOutboxAppend a pending envelope and return OutboxAppendResult with the insertion outcome
IOutboxLeaseStoreIOutboxProcessorAtomically claim due messages
IOutboxStateWriterIOutboxProcessorPersist published, failed, or dead-lettered envelopes
IOutboxDeadLetterStoreOperator toolingRequeue dead-lettered messages
IOutboxRetentionStoreCleanup servicesDelete published messages past retention
IOutboxDiagnosticsStoreOperatorsStatus counts

Retry and Dead Letter

outbox.UseProcessorOptions(new OutboxProcessorOptions
{
    BatchSize = 100,
    LeaseDuration = TimeSpan.FromMinutes(2),
    Retry = new RetryOptions
    {
        MaxAttempts = 12,
        InitialDelay = TimeSpan.FromSeconds(10),
        MaxDelay = TimeSpan.FromMinutes(10),
        Backoff = RetryBackoff.Exponential,
        UseJitter = true
    }
});

Retry.MaxAttempts counts dispatch attempts recorded on the leased envelope. Requeue operations through IOutboxManager.RequeueAsync return RequeueResult { Requested, Requeued }.

After-dispatch hook failures follow HookFailurePolicy on OutboxProcessorOptions:

DispatcherDefault HookFailurePolicyBehavior after successful publish
Transport (UseAmqpDispatch, UseInMemoryDispatch, and siblings)CompleteDespiteHookFailureLogs the hook error and persists published state without re-publishing
In-process (UseInProcessDispatch)DeadLetterMoves the row to dead letter without re-publishing

Set HookFailurePolicy = ProcessorHookFailurePolicy.DeadLetter on transport dispatchers when hook side effects must block terminal completion. Set HookFailurePolicy = ProcessorHookFailurePolicy.CompleteDespiteHookFailure on in-process dispatch when hook persistence should not undo a successful handler publication. Call UseProcessorOptions after dispatcher registration to override the dispatcher default.

Transport dispatchers (UseAmqpDispatch, UseAwsSqsDispatch, and siblings) accept ValidatePayloadBeforeDispatch on their options. When false (default), invalid JSON is not deserialized before publish; when true, deserialization runs before the transport call so corrupt payloads fail fast without broker side effects.

Batch enqueue serializes items in parallel inside OutboxEnvelopeFactory.CreateBatchAsync.

Next

On this page