LiteBus
Architecture

Architecture

LiteBus is an in-process mediator with semantic modules for commands, queries, and events. v6 adds durable messaging through orthogonal Storage, Dispatch, and Ingress adapters without turning the mediator into a broker.

Architecture Roles

RoleResponsibilityMain packages
Platform contractsContainer-neutral modules, dispatch scope, and transport contractsLiteBus.Runtime.Abstractions, LiteBus.Transport.Abstractions
Mediation contractsMessage and semantic handler contractsLiteBus.Messaging.Abstractions, command/query/event abstractions
Durable contractsDurable metadata, leases, processor hooks, and axis contractsLiteBus.DurableMessaging.Abstractions, inbox/outbox/saga abstractions
Core implementationsModule runtime, mediation, semantic mediators, and durable processorsLiteBus.Runtime, LiteBus.Messaging, LiteBus.Commands, LiteBus.Queries, LiteBus.Events, LiteBus.Inbox, LiteBus.Outbox, LiteBus.Saga
Technology adaptersBroker and persistence SDK ownershipLiteBus.Transport.*, LiteBus.Storage.PostgreSql, LiteBus.Storage.EntityFrameworkCore
Feature bridgesStorage, dispatch, ingress, and saga-to-inbox mappingLiteBus.Inbox.*, LiteBus.Outbox.*, LiteBus.Saga.InboxIntegration, LiteBus.Saga.Storage.* adapters
Host adaptersDI, hosting, telemetry, health, and ASP.NET Core mappingLiteBus.Runtime.Extensions.*, LiteBus.*.Extensions.*, LiteBus.Extensions.*
Consumer toolingAnalyzer and application test supportLiteBus.Analyzers, LiteBus.Testing, LiteBus.Testing.Mediation, LiteBus.Testing.Transport, LiteBus.Testing.DurableMessaging, LiteBus.Testing.Hosting

Startup Model

Applications call the single AddLiteBus(Action<ILiteBusBuilder>) entry point and configure package extensions on ILiteBusBuilder from LiteBus.Runtime.Abstractions. The builder exposes only Modules; normal application code uses package-owned extensions such as AddMessaging, AddCommands, AddInbox, and AddOutbox. Advanced composition uses builder.Modules rather than a second host-adapter overload. Each module contributes dependency descriptors, and the selected container adapter translates them into registrations.

IModuleRegistry.BuildOrder() resolves modules in topological order and freezes further registration. AddLiteBus calls BuildOrder() after the configuration callback returns.

Manifest startup tasks and background services are executed by a single generic-host orchestrator (LiteBusHostOrchestrator). Startup tasks run sequentially during StartAsync; background service loops start only after every startup task succeeds. A startup task failure aborts host startup and leaves background services unstarted.

Core inbox and outbox modules register writers and processors only. Storage, dispatch, and ingress are separate registrations. Processor background services fail fast when no dispatcher is registered.

Microsoft DI and Autofac each provide one IMessageDispatchScopeFactory. Messaging composition fails when no dispatch-scope adapter is registered. Manual hosts can select RootMessageDispatchScopeFactory explicitly when root-provider dispatch is acceptable.

Mediation Pipeline

Commands, queries, and events use the messaging layer with type-specific rules.

Message kindEntry pointHandler rule
Command without resultICommandMediator.SendAsync(ICommand)exactly one main handler
Command with resultICommandMediator.SendAsync(ICommand<TResult>)exactly one main handler
QueryIQueryMediator.QueryAsyncexactly one main handler
Stream queryIQueryMediator.StreamAsyncexactly one stream handler
EventIEventMediator.PublishAsynczero or more handlers

Pre-handlers, post-handlers, and error handlers wrap the main handler path. Event handlers can be ordered by priority and executed sequentially or concurrently.

API Model Conventions

LiteBus names public value objects and operation inputs with a fixed suffix taxonomy (*Item, *Metadata, *Request, *Settings, *Options, and related patterns). Durable writers accept InboxAcceptItem / OutboxEnqueueItem with per-message metadata; mediation keeps *Settings for per-invocation pipeline tuning.

  • Rules (suffix taxonomy, banned shapes, parameter budget, mapping boundary): AGENTS.md under API and value object design
  • Concrete inventory (named types, package map, writer signatures, remaining API alignment): API design reference

When adding or renaming public types, match AGENTS.md first; use API-Design.md for examples and placement.

Storage Axis

Storage adapters implement narrow store roles against one logical table or DbSet. PostgreSQL, EF Core, and InMemory implementations register one singleton instance for all roles on that store.

Implementation Invariants

  • IsAvailable must guard LeaseExpiresAt is not null when status is processing or publishing.
  • trace_context must be wired end-to-end when present in schema (envelopes, adapter mappings, SQL, lease rows).
Accept path (auto-commit)
  IInbox.AcceptAsync(InboxAcceptItem) / IOutbox.EnqueueAsync(OutboxEnqueueItem)
    -> singleton IInboxStore / IOutboxStore (commits immediately)

Accept path (participates in caller transaction)
  ITransactionalInbox.AcceptAsync(InboxAcceptItem) / ITransactionalOutbox.EnqueueAsync(OutboxEnqueueItem)
    or ITransactionalInbox<TContext> / ITransactionalOutbox<TContext> (EF)
    -> bound ITransactionalInboxStore / ITransactionalOutboxStore
    -> persisted InboxEnvelope / OutboxEnvelope when caller commits
    See [Transactional messaging writes](../reliable-messaging/transactional-writes.md).

Process path
  IInboxProcessor / IOutboxProcessor
    -> IInboxLeaseStore / IOutboxLeaseStore.LeasePendingAsync
    -> dispatch leased envelope
    -> IInboxStateWriter / IOutboxStateWriter.PersistAsync (completed, retry, dead-letter)

Operations path
  IInboxDeadLetterStore / IOutboxDeadLetterStore (manual replay)
  IInboxRetentionStore / IOutboxRetentionStore (delete aged terminal rows)
  IInboxDiagnosticsStore / IOutboxDiagnosticsStore (status counts)
flowchart LR
  subgraph accept [Accept]
    W["IInbox / IOutbox (InboxAcceptItem / OutboxEnqueueItem)"]
    S[(IInboxStore / IOutboxStore)]
    W --> S
  end

  subgraph process [Process]
    P[IInboxProcessor / IOutboxProcessor]
    L[IInboxLeaseStore / IOutboxLeaseStore]
    ST[IInboxStateWriter / IOutboxStateWriter]
    P --> L
    P --> ST
  end

  subgraph ops [Operations]
    DL[IInboxDeadLetterStore / IOutboxDeadLetterStore]
    RT[IInboxRetentionStore / IOutboxRetentionStore]
    DG[IInboxDiagnosticsStore / IOutboxDiagnosticsStore]
  end

  S --> L
CapabilityInbox rolesOutbox roles
Append accepted workIInboxStoreIOutboxStore
Lease due workIInboxLeaseStoreIOutboxLeaseStore
Persist processor outcomesIInboxStateWriterIOutboxStateWriter
Requeue dead-lettered rowsIInboxDeadLetterStoreIOutboxDeadLetterStore
Delete aged terminal rowsIInboxRetentionStoreIOutboxRetentionStore
Queue diagnosticsIInboxDiagnosticsStoreIOutboxDiagnosticsStore
Query and purge rowsIInboxMessageQuery, IInboxPurgeStoreIOutboxMessageQuery, IOutboxPurgeStore

Fine-grained interfaces remain for callers that depend on one concern. Composite roles group the slices processors and operator tooling need:

Composite roleExtendsUsed by
IInboxProcessingStoreIInboxLeaseStore, IInboxStateWriterPipelinedInboxProcessor
IInboxOperationsStoredead-letter, retention, diagnostics, query, purge rolesIInboxManager, retention and cleanup services
IOutboxProcessingStoreIOutboxLeaseStore, IOutboxStateWriterPipelinedOutboxProcessor
IOutboxOperationsStoredead-letter, retention, diagnostics, query, purge rolesIOutboxManager, retention and cleanup services

PostgreSQL and EF Core stores implement all roles on one implementation class. InMemory stores do the same for tests.

Retention Cutoff Semantics

Cleanup services call IInboxRetentionStore.DeleteCompletedOlderThanAsync and IOutboxRetentionStore.DeletePublishedOlderThanAsync with a cutoff timestamp. Implementations compare the cutoff against the effective terminal timestamp:

StoreEffective timestampSQL expression
InboxCompletion time when recorded, otherwise acceptance timeCOALESCE(completed_at, created_at)
OutboxPublication time when recorded, otherwise enqueue timeCOALESCE(published_at, created_at)

Current inbox and outbox schemas include completed_at, published_at, and operational history columns such as last_attempted_at and dead_lettered_at. Retention deletes rows based on when work finished, not when it was first accepted. A message accepted long ago but completed recently stays until the cutoff passes its completion time.

OpenTelemetry Metric Catalog

Instrument names are stable public constants on LiteBusInboxTelemetry, LiteBusOutboxTelemetry, LiteBusInboxIngressTelemetry, and LiteBusTransportTelemetry. Register meters with AddLiteBusInboxMetrics(), AddLiteBusOutboxMetrics(), and AddLiteBusTransportMetrics() from the matching *.Extensions.OpenTelemetry packages. Ingress counters use the same LiteBus.Inbox meter as inbox processor metrics, so AddLiteBusInboxMetrics() exports both. Transport circuit breaker gauges are recorded on the shared LiteBus.Transport meter with the litebus.transport.broker dimension identifying the active adapter (amqp, kafka, sqs, azure_service_bus, inmemory). AddLiteBusAmqpMetrics() registers the same shared meter for backward compatibility.

MeterInstrumentPublic constantDescription
LiteBus.Inboxlitebus.inbox.queue.depthLiteBusInboxTelemetry.QueueDepthInstrumentNameObservable gauge; tag litebus.inbox.status
LiteBus.Inboxlitebus.inbox.processor.stateLiteBusInboxTelemetry.ProcessorStateInstrumentName0 Running, 1 Paused, 2 Draining
LiteBus.Inboxlitebus.inbox.processor.persist_skippedLiteBusInboxTelemetry.ProcessorPersistSkippedInstrumentNameSkipped terminal persist when lease owner no longer matches
LiteBus.Inboxlitebus.inbox.processor.persist_failedLiteBusInboxTelemetry.ProcessorPersistFailedInstrumentNameTerminal persist threw after successful handler dispatch
LiteBus.Inboxlitebus.inbox.diagnostics.unavailableLiteBusInboxTelemetry.DiagnosticsUnavailableInstrumentNameQueue depth probe failed against the backing store
LiteBus.Inboxingress.ack_failed_after_acceptLiteBusInboxIngressTelemetry.AckFailedAfterAcceptInstrumentNameBroker acknowledgement failed after a successful inbox accept
LiteBus.Outboxlitebus.outbox.processor.persist_skippedLiteBusOutboxTelemetry.ProcessorPersistSkippedInstrumentNameSkipped terminal persist when lease owner no longer matches
LiteBus.Outboxlitebus.outbox.processor.persist_failedLiteBusOutboxTelemetry.ProcessorPersistFailedInstrumentNameTerminal persist threw after successful broker publish
LiteBus.Outboxlitebus.outbox.diagnostics.unavailableLiteBusOutboxTelemetry.DiagnosticsUnavailableInstrumentNameQueue depth probe failed against the backing store
LiteBus.Outboxlitebus.outbox.queue.depthLiteBusOutboxTelemetry.QueueDepthInstrumentNameObservable gauge; tag litebus.outbox.status
LiteBus.Outboxlitebus.outbox.processor.stateLiteBusOutboxTelemetry.ProcessorStateInstrumentNameSame encoding as inbox processor state
LiteBus.Transportlitebus.transport.circuit_breaker.openLiteBusTransportTelemetry.CircuitBreakerOpenInstrumentName1 when any publisher circuit is open or half-open; tag litebus.transport.broker
LiteBus.Transportlitebus.transport.circuit_breaker.failure_countLiteBusTransportTelemetry.CircuitBreakerFailureCountInstrumentNameCurrent failure sum across publisher destinations; tag litebus.transport.broker
LiteBus.Transport.AzureServiceBus(reserved)Adapter meter identity for custom instrumentation
LiteBus.Transport.AwsSqs(reserved)Adapter meter identity for custom instrumentation
LiteBus.Transport.Kafka(reserved)Adapter meter identity for custom instrumentation
LiteBus.Transport.InMemory(reserved)Adapter meter identity for test and CI transports

Processor pass counters below are recorded by internal processor instrumentation (InboxProcessorTelemetry, OutboxProcessorTelemetry). Names are stable for metric export but are not public constants; dashboards may reference them, yet renames are not governed by the public instrument contract until promoted to LiteBusInboxTelemetry or LiteBusOutboxTelemetry.

MeterInstrumentDescription
LiteBus.Inboxlitebus.inbox.processor.passesOne increment per processor pass
LiteBus.Inboxlitebus.inbox.processor.succeededEnvelopes marked completed during a pass
LiteBus.Inboxlitebus.inbox.processor.failedEnvelopes scheduled for retry during a pass
LiteBus.Inboxlitebus.inbox.processor.dead_letteredEnvelopes moved to dead letter during a pass
LiteBus.Inboxlitebus.inbox.processor.loop_errorsUnhandled exceptions caught by the processor background loop
LiteBus.Outboxlitebus.outbox.processor.passesOne increment per processor pass
LiteBus.Outboxlitebus.outbox.processor.publishedMessages marked published during a pass
LiteBus.Outboxlitebus.outbox.processor.failedMessages scheduled for retry during a pass
LiteBus.Outboxlitebus.outbox.processor.dead_letteredMessages moved to dead letter during a pass
LiteBus.Outboxlitebus.outbox.processor.loop_errorsUnhandled exceptions caught by the processor background loop

Breaking change (v6): litebus.amqp.circuit_breaker.* instruments on the LiteBus.Transport.Amqp meter were removed. Use litebus.transport.circuit_breaker.* on LiteBus.Transport with litebus.transport.broker="amqp".

Framework-neutral diagnostic probes use IDiagnosticCheck and LiteBusHostManifest. Applications map probe results to health endpoints or custom sinks.

Transport Platform

Broker clients implement ITransportPublisher and IMessageConsumer from LiteBus.Transport.Abstractions. Shared dispatch logic lives in LiteBus.Inbox.Dispatch and LiteBus.Outbox.Dispatch. Broker transports are root modules shared by dispatch and ingress adapters. Shared circuit breaker metrics live in LiteBus.Transport; transport header value parsing lives in LiteBus.Transport.Abstractions; envelope header mapping lives in the dispatch and ingress adapter packages.

PackageRoleBrokerDestination mapping
LiteBus.Transport.AmqpTechnology adapterRabbitMQPublish: Destination = exchange, Route = routing key. Consume: Destination = queue name, Route = routing key
LiteBus.Transport.AzureServiceBusTechnology adapterAzure Service BusDestination = queue or topic; Route = subject
LiteBus.Transport.AwsSqsTechnology adapterAmazon SQSDestination = queue URL
LiteBus.Transport.InMemoryTechnology adapterSystem.Threading.ChannelsDestination = logical queue name
LiteBus.Transport.KafkaTechnology adapterApache Kafka (Confluent)Destination = topic; Route = record key

Register one transport module per process. Each module registers ITransportPublisher, IMessageConsumer, and destination-scoped publisher circuit breakers exposed through the shared LiteBus.Transport metrics.

services.AddLiteBus(bus =>
{
    bus.AddAmqpTransport(new AmqpConnectionOptions
    {
        Uri = new Uri(configuration["Amqp:Uri"]!)
    });
    bus.AddMessaging(_ => { });

    bus.AddInbox(inbox => inbox.UseAmqpDispatch(options =>
    {
        options.DefaultDestination = "orders.commands";
    }));

    bus.AddOutbox(outbox => outbox.UseAmqpDispatch(options =>
    {
        options.DefaultDestination = "orders.events";
    }));
});

Kafka At-Least-Once Semantics

LiteBus.Transport.Kafka uses manual offset commit. TransportMessage.AcceptAsync commits the consumed offset. ReturnToQueueAsync seeks the consumer back to the failed offset before the next read so the record is retried in-session. DiscardAsync leaves the offset uncommitted until consumer restart or rebalance. Kafka has no queue-style lease equivalent. Pair Kafka ingress with inbox idempotency keys and treat handlers as idempotent.

Transport Resilience Capability Matrix

Minimum acknowledgement and recovery behavior implemented by each adapter. Raw IMessageConsumer hosts use TransportConsumerHandlerInvoker, which returns failed deliveries to the broker and continues the consume loop. Inbox ingress applies its own poison, retry, and dead-letter policy on top of transport deliveries.

ProviderAck (AcceptAsync)Requeue (ReturnToQueueAsync)Handler throw (raw consumer)Consumer reconnect
AMQP (RabbitMQ)basic.ackbasic.nack with requeueAuto-nack with requeue via shared invokerChannel shutdown signals stop; caller restarts consumer
InMemoryRemove from channelRe-enqueue on channelRe-enqueue via shared invokerN/A (in-process)
Azure Service BusCompleteMessageAbandonMessageAbandon via shared invokerExponential backoff restart loop in AzureServiceBusConsumer
AWS SQSDeleteMessageChangeMessageVisibility with backoffRequeue via shared invokerPoll loop continues after receive errors
KafkaOffset commitSeek to failed offset with backoffSeek via shared invoker; offset not advancedConsumer restart / rebalance redelivers uncommitted offsets

Poison Message Handling

HostBehavior
Raw IMessageConsumerHandler exceptions requeue indefinitely unless the handler calls DiscardAsync or the broker dead-letters after its own redelivery limits. No LiteBus redelivery cap is applied at the transport layer.
Inbox ingressMaps transport deliveries to inbox accept. Transient failures can requeue at the broker when RequeueOnFailure is enabled. Poison and contract failures are discarded or dead-lettered according to IngressAckPolicy and inbox processor retry limits.
Application guidanceUse inbox ingress when you need idempotency, store-backed retries, and dead-lettering. Use raw consumers only when the application owns retry caps, poison queues, and idempotent handlers.

AMQP Transport Bootstrap

AddAmqpTransport registers the single root AmqpTransportModule. UseAmqpDispatch and UseAmqpIngress only configure feature bridges. Their modules declare IRequires<AmqpTransportModule>, so composition rejects a missing root transport before any module builds. Kafka, AWS SQS, Azure Service Bus, and in-memory adapters use the same ownership rule.

Inbox composite child order remains storage, dispatcher, then ingress for same-level children. Static module dependencies take precedence when topological sorting requires a lower-level transport module first.

Dispatch Tracing

Concrete publishers emit send {destination} producer activities. TransportConsumerHandlerInvoker emits process {destination} consumer activities. Both use the LiteBus.Transport activity source and the OpenTelemetry messaging attributes documented in Transport Tracing. Register the source with AddLiteBusTransportInstrumentation().

Dispatch Axis

Dispatch adapters turn a leased envelope into a side effect. Core processors call IInboxDispatcher.DispatchAsync or IOutboxDispatcher.DispatchAsync and record success or failure based on thrown exceptions.

Inbox processor
  -> IInboxDispatcher.DispatchAsync(InboxEnvelope)
     -> Inbox.Dispatch.InProcess: deserialize, require ICommand, ICommandMediator.SendAsync
     -> Inbox.Dispatch + *.Dispatch.Amqp / *.Dispatch.InMemory: publish via ITransportPublisher

Outbox processor
  -> IOutboxDispatcher.DispatchAsync(OutboxEnvelope)
     -> Outbox.Dispatch.InProcess: deserialize, IEventMediator.PublishAsync
     -> Outbox.Dispatch + *.Dispatch.Amqp / *.Dispatch.InMemory: publish via ITransportPublisher with contract headers
flowchart TB
  IP[IInboxProcessor]
  ID[IInboxDispatcher]
  INPROC[CommandInboxDispatcher]
  AMQPI[TransportInboxDispatcher + AmqpTransport]
  IP --> ID
  ID --> INPROC
  ID --> AMQPI

  OP[IOutboxProcessor]
  OD[IOutboxDispatcher]
  OUTINPROC[EventOutboxDispatcher]
  AMQPO[TransportOutboxDispatcher + AmqpTransport]
  OP --> OD
  OD --> OUTINPROC
  OD --> AMQPO

Register exactly one dispatcher per durable path. Processor background services validate this at startup.

Processor Pipeline

Inbox and outbox use PipelinedInboxProcessor and PipelinedOutboxProcessor exclusively. Each pass leases a batch, fans envelopes to DispatcherConcurrency workers, renews leases on a heartbeat interval, and persists terminal outcomes immediately.

Each acquisition increments lease_generation. Renewal and terminal persistence require both the current owner and generation, so an earlier attempt cannot complete after expiry and reacquisition, even when the configured owner label is unchanged. Direct PostgreSQL and relational EF Core stores use database time for lease eligibility and expiry; in-memory stores use the supplied TimeProvider for deterministic tests.

Pass Abort vs Per-Message Persistence

Dispatch failures and lease-loss outcomes are isolated per envelope: one worker records its outcome and continues with the next leased message. Terminal persistence follows the same per-message containment rule. When PersistTerminalOutcomeAsync throws for one envelope, PipelinedMessageProcessor logs the failure and continues the pass so sibling workers can finish dispatch and persistence for the remaining batch. The failed envelope keeps its active lease until expiration and becomes eligible for a later pass.

Pass-level abort still applies to leasing, channel fan-out, and cooperative shutdown. OperationCanceledException from the pass cancellation token stops the worker without swallowing the cancellation. Uncaught exceptions from DispatchEnvelopeAsync still propagate from the worker and can fault the pass when they are not the lease-loss cancellation pattern handled explicitly in RunWorkerAsync.

OptionDefaultRole
DispatcherConcurrency1Parallel dispatch workers per processor instance
LeaseHeartbeatInterval15 secondsRenews active leases during slow handlers (must be <= LeaseDuration / 2)
HonorShutdownTokenOnPersistfalseWhen false, terminal PersistAsync uses CancellationToken.None; when true, passes shutdown token (faster drain, duplicate-dispatch risk)
HookFailurePolicyDispatcher-specificTransport outbox dispatch defaults to CompleteDespiteHookFailure; in-process outbox and inbox processors default to DeadLetter. See Outbox and Inbox.
TenantIdnullLimits leasing to one tenant partition

IProcessorEnvelopeHook implementations from LiteBus.DurableMessaging.Abstractions run around each leased envelope. Inbox and outbox map envelopes through adapters. EnableSaga() registers saga hooks without coupling LiteBus.Saga to inbox abstractions.

Set DispatcherConcurrency above 1 only when handlers are safe to run in parallel. Background services create a per-message DI scope so scoped handlers (for example DbContext) resolve correctly.

Delivery Semantics (At-Least-Once)

Durable paths guarantee at-least-once execution and publication, not exactly-once side effects. Handlers, dispatchers, and downstream consumers must be idempotent or deduplicate with IdempotencyKey and application logic. A crash between external side effect and terminal persist can produce duplicates. Outbox processors publish through IOutboxDispatcher before terminal persist; a crash or skipped persist after broker ack can republish the same envelope on lease reclaim.

Transactional EF Behavior

Idempotency keys are scoped per tenant across in-memory, EF Core, and PostgreSQL stores. The same key under different tenant_id values persists independently; null or whitespace tenants normalize to an empty string. On the transactional EF path, duplicate message_id or (tenant_id, idempotency_key) conflicts cause SaveChanges to fail and roll back the entire unit of work. Provider-specific upsert for silent idempotent inserts is not shipped; see Roadmap.

Encryption Rotation

IPayloadEncryptor key rotation is an application responsibility. LiteBus stores ciphertext as opaque bytes; plan re-encryption or multi-key decrypt outside the framework.

Instrument Stability

Public const instrument and activity names on LiteBusInboxTelemetry, LiteBusOutboxTelemetry, LiteBusInboxIngressTelemetry, LiteBusTransportTelemetry, and adapter meter types are consumer contract. Renames or removals are breaking changes. Processor pass counters listed in the internal-only catalog above are stable implementation details until promoted to public constants on the matching telemetry type.

Ingress Axis

Ingress adapters accept messages from external transports and write them through IInbox.AcceptAsync. They do not execute handlers directly.

AMQP queue
  -> TransportInboxIngressConsumer (IBackgroundService)
  -> TransportInboxIngressHandler maps TransportMessage headers + body to InboxAcceptItem
    -> IInbox.AcceptAsync(InboxAcceptItem)
    -> ack/nack on store acceptance

Later (same process or fleet)
  -> IInboxProcessor
    -> IInboxDispatcher (typically InProcess)
    -> handler execution
flowchart LR
  Q[AMQP queue]
  IG[Inbox.Ingress.Amqp]
  IB[IInbox.AcceptAsync]
  ST[(Inbox store)]
  PR[IInboxProcessor]
  DS[IInboxDispatcher]

  Q --> IG --> IB --> ST
  ST --> PR --> DS

Ingress depends on inbox core and a registered IMessageConsumer implementation. It does not reference storage implementation types directly beyond what DI provides through IInbox.

End-to-End Pipeline Example

A common integration flow combines ingress, command dispatch, domain logic, outbox storage, and AMQP publication:

External producer
  -> AMQP -> Ingress -> IInbox -> store
  -> InboxProcessor -> Command dispatcher -> command handler
  -> handler writes domain state + IOutbox.EnqueueAsync in same DB transaction
  -> OutboxProcessor -> Amqp dispatcher -> integration exchange

Handlers and dispatch targets must be idempotent. Inbox and outbox provide at-least-once behavior, not exactly-once side effects.

Durable Messaging Glossary

TermMeaning
InboxStatus.PendingAccepted and waiting for a processor lease.
InboxStatus.ProcessingLeased by one worker; dispatch is in progress or the lease has not yet expired.
InboxStatus.CompletedHandler finished successfully; row is terminal until retention deletes it.
InboxStatus.FailedDispatch threw; eligible for retry after the visible-after timestamp.
InboxStatus.DeadLetteredRetry policy exhausted or manually moved aside; use IInboxDeadLetterStore.RequeueAsync to replay.
OutboxStatus.PendingEnqueued and waiting for a publisher lease.
OutboxStatus.PublishingLeased by one publisher; dispatch is in progress or the lease has not yet expired.
OutboxStatus.PublishedPublished successfully; row is terminal until retention deletes it.
OutboxStatus.FailedPublication threw; eligible for retry after the visible-after timestamp.
OutboxStatus.DeadLetteredRetry policy exhausted or manually moved aside; use IOutboxDeadLetterStore.RequeueAsync to replay.
LeaseTemporary row ownership recorded by IInboxLeaseStore or IOutboxLeaseStore. A lease sets owner identity, expiration time, and attempt count. Other workers must not receive the same envelope while the lease is active. Expired leases become eligible again, which gives crash recovery without exactly-once guarantees.
At-least-onceInbox and outbox may deliver the same logical message more than once after worker failure, lease expiry, or broker redelivery. Handlers, dispatchers, and downstream consumers must be idempotent or deduplicate with Idempotency on InboxAcceptMetadata / OutboxEnqueueMetadata.

Contracts and Serialization

Stored rows contain contract name, contract version, and serialized payload. The default registry maps concrete CLR types to names and versions. The default serializer uses System.Text.Json with web defaults.

Closed generic messages are supported when each closed type has a registered contract. Open generic contracts are rejected.

Saga Orchestration

Saga integrates through inbox.EnableSaga(...) from LiteBus.Saga.InboxIntegration. SagaModuleBuilder owns exactly one explicit storage module. Use UseInMemoryStorage() for tests or select a persistence adapter; missing or duplicate storage fails composition.

PackageRoleResponsibility
LiteBus.DurableMessaging.AbstractionsDurable contractsIProcessorEnvelopeHook, durable metadata, retry, and lease contracts
LiteBus.Saga.AbstractionsDurable contractsISagaStore, ISagaContext, saga state registry
LiteBus.SagaCore implementationSagaProcessorHook, SagaModule, and SagaModuleBuilder
LiteBus.Saga.InboxIntegrationFeature bridgeNested EnableSaga(...) on InboxModuleBuilder
LiteBus.Saga.Storage.PostgreSqlFeature bridgePostgreSQL saga instance persistence
builder.AddMessaging(_ => { });
builder.AddInbox(inbox =>
{
    inbox.Contracts.Register<AdvanceOrderSagaCommand>("orders.saga.advance", 1);
    inbox.EnableSaga(saga =>
    {
        saga.RegisterState<OrderSagaState>("orders.saga");
        saga.MapContract("orders.saga.advance", "orders.saga");
        saga.UsePostgreSqlStorage(pg => pg.UseDataSource(dataSource));
    });
});

Saga state is command-centric today (LiteBus.Saga.Abstractions references command contracts). Applications own state POCOs and completion rules.

Concurrency and Scope

SagaExecutionContext registers as a singleton with per-dispatch scope on the instance for the processor before/dispatch/after flow. Optimistic concurrency uses versioned saves. A dirty-state conflict propagates as SagaConcurrencyException because the hook cannot safely merge arbitrary handler-owned state with a concurrent update. Completion-only conflicts may reload and retry up to three attempts because completion is idempotent. Inbox terminal persistence and saga save are separate steps; handlers must tolerate at-least-once redelivery. Transactional inbox + saga commit in one connection is deferred (see Saga durability model).

Message Registry Lifecycle

v6 uses one IMessageRegistry per IModuleConfiguration instance created during AddLiteBus. Modules register message and handler types at compose time through MessageModule and semantic module builders. The registry is not process-wide: tests and hosts each get an isolated instance. Do not call IMessageWriter.Register at runtime in application code; register types during module Build() or through RegisterFromAssembly on module builders.

Payload Encryption and Tenant Isolation

IPayloadEncryptor protects serialized payload bodies at rest. Register it through UsePayloadEncryption() on inbox and outbox module builders. Contract name and version remain plaintext so leasing, routing, and diagnostics continue to work.

ITenantRoutingStrategy.ResolveRoute optionally computes a transport publish route from tenant metadata. Processor leasing is a separate durable concern: set TenantScope on accept or enqueue metadata and TenantId on processor options to scope lease queries in PostgreSQL, EF Core, and InMemory stores.

inbox.UsePayloadEncryption(myEncryptor);
outbox.UsePayloadEncryption(myEncryptor);

inbox.UseProcessorOptions(new InboxProcessorOptions { TenantId = "tenant-a" });
inbox.UseAmqpDispatch(options =>
{
    options.DefaultDestination = "commands";
});

Composite Store Roles

Fine-grained store interfaces remain public. Processors and managers depend on composite roles that storage adapters register on the same singleton instance:

CompositeCombinesUsed by
IInboxProcessingStorelease, state writerIInboxProcessor
IInboxOperationsStoredead-letter, retention, diagnostics, query, purgeIInboxManager
IOutboxProcessingStorelease, state writerIOutboxProcessor
IOutboxOperationsStoredead-letter, retention, diagnostics, query, purgeIOutboxManager

Module Composition

Parent modules use ICompositeModule to declare storage, dispatch, ingress, and saga children. Child membership is collected before build. BuildOrder() validates every IRequires<TModule> edge, inserts composite children, and topologically sorts the complete graph before any Build() call. Configuration context stores shared configuration objects only; registration markers and dependency-registry scans are not part of ordering. Duplicate registration of the same module type throws LiteBusConfigurationException at compose time.

Register adapters inside the parent builder:

builder.Services.AddLiteBus(builder =>
{
    builder.AddAmqpTransport(new AmqpConnectionOptions
    {
        Uri = new Uri(configuration["Amqp:Uri"]!)
    });
    builder.AddMessaging(_ => { });
    builder.AddInbox(inbox =>
    {
        inbox.UsePostgreSqlStorage(pg => pg.UseDataSource(dataSource));
        inbox.UseAmqpDispatch(options =>
        {
            options.DefaultDestination = "commands";
        });
        inbox.EnableInboxProcessor();
    });
});

Do not add top-level IModuleRegistry shortcuts that bypass the parent builder. v6 removed flat storage and dispatcher registrars; use nested Use* extensions only.

Deferred Capabilities (Not Shipped)

Documented in Roadmap: built-in idempotency consumer middleware, per-contract IRetryClassifier, SQS FIFO and broker DLQ integration, multi-bus named instances, schema drift CLI, payload compression, and aggregate validation/authorization/caching LiteBus. Store-side visible_after covers delayed processing; broker-native delayed delivery beyond that is not shipped.

Boundaries

  • LiteBus mediates in process. Broker clients live in LiteBus.Transport.* adapter packages and are consumed by dispatch/ingress transport adapters.
  • Core inbox and outbox do not reference Commands, Events, Npgsql, EF, or RabbitMQ.
  • DI adapters translate descriptors; mediator code does not depend on Microsoft DI or Autofac.
  • LiteBus.Analyzers validates registration patterns at compile time without runtime coupling.

Next

Follow package boundaries in the Dependency Graph. For upgrade steps, read Migration Guide v6.

On this page