Skip to content

Architecture

A. Shafie edited this page May 29, 2026 · 3 revisions

Architecture

LiteBus is an in-process mediator with semantic modules for commands, queries, and events. Inbox and outbox modules add storage boundaries without turning the mediator into a broker.

Core Layers

Layer Responsibility Main packages
Runtime Container-neutral module and dependency registration LiteBus.Runtime.Abstractions, LiteBus.Runtime
Messaging Message registry, handler descriptors, mediation pipeline, execution context, contract registry LiteBus.Messaging.Abstractions, LiteBus.Messaging
Semantic modules Command, query, and event mediators with type-specific APIs LiteBus.Commands, LiteBus.Queries, LiteBus.Events
Storage modules Explicit command scheduling and event publication storage LiteBus.Inbox, LiteBus.Outbox
Infrastructure packages Optional DI and storage adapters Microsoft DI, Autofac, PostgreSQL packages

Startup Model

Applications call AddLiteBus and register modules. Each module contributes dependency descriptors to the runtime registry. The selected DI adapter translates those descriptors into container registrations.

Message modules also register message and handler metadata. RegisterFromAssembly scans for concrete handlers, concrete messages, and supported open generic handlers. Registration order does not matter because the message registry links handlers to messages after each registration.

Mediation Pipeline

Commands, queries, and events all use the messaging layer, but each semantic module chooses its own strategy.

Message kind Entry point Handler rule Result rule
Command without result ICommandMediator.SendAsync(ICommand) exactly one main handler returns Task
Command with result ICommandMediator.SendAsync(ICommand<TResult>) exactly one main handler returns Task<TResult>
Query IQueryMediator.QueryAsync exactly one main handler returns the query result
Stream query IQueryMediator.StreamAsync exactly one stream handler returns IAsyncEnumerable<TResult>
Event IEventPublisher.PublishAsync zero or more handlers returns after selected handlers complete

Pre-handlers run before the main handler. Post-handlers run after the main handler. Error handlers run when the main handler path throws. Event handlers can be ordered by priority and executed sequentially or concurrently according to event mediation settings.

Execution Context

The messaging layer creates an ambient execution context for each mediation call. Handlers can read tags, cancellation, and Items from AmbientExecutionContext.Current. The context uses async flow, so it follows awaited work and stream query enumeration.

Use context data for technical policy such as tracing, tenant id, metrics, handler filters, or inbox execution flags. Keep business data in the message contract.

Command Inbox Flow

The command inbox has a separate API from the command mediator.

caller
  -> ICommandScheduler.ScheduleAsync
    -> IMessageContractRegistry.GetContract(runtime command type)
    -> IMessageSerializer.SerializeAsync
    -> ICommandInboxWriter.AddAsync
    -> CommandReceipt<TCommand>

worker
  -> ICommandInboxProcessor.ProcessPendingAsync
    -> ICommandInboxLeaseStore.LeasePendingAsync
    -> IMessageContractRegistry.GetMessageType(contract name, version)
    -> IMessageSerializer.DeserializeAsync
    -> ICommandMediator.SendAsync
    -> ICommandInboxStateStore.MarkCompletedAsync / MarkFailedAsync / MoveToDeadLetterAsync

SendAsync always executes immediately. ScheduleAsync always stores for later execution. Queries are never scheduled.

Outbox Flow

The outbox has a separate API from event mediation.

application transaction
  -> IIntegrationOutbox.AddAsync or IOutboxWriter.AddAsync
    -> IMessageContractRegistry.GetContract(runtime event type)
    -> IMessageSerializer.SerializeAsync
    -> IOutboxMessageWriter.AddAsync
    -> OutboxReceipt<TEvent>

publisher
  -> IOutboxProcessor.ProcessPendingAsync
    -> IOutboxMessageLeaseStore.LeasePendingAsync
    -> IOutboxDispatcher.DispatchAsync
    -> IOutboxMessageStateStore.MarkPublishedAsync / MarkFailedAsync / MoveToDeadLetterAsync

PublishAsync notifies in-process handlers now. AddAsync stores an event for later publication. The LiteBus event dispatcher can replay stored events into the local event pipeline. Broker dispatchers can publish the same envelope to external transports.

Store Role Split

Stores are split by capability.

Capability Inbox Outbox
Append accepted work ICommandInboxWriter IOutboxMessageWriter
Lease due work ICommandInboxLeaseStore IOutboxMessageLeaseStore
Record result state ICommandInboxStateStore IOutboxMessageStateStore

The PostgreSQL stores implement all roles because one table owns the data. Application services and processors still depend on narrow interfaces.

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 because a stored row must map to one concrete CLR type during deserialization.

Retry and Dead Letter

Processors own retry timing. Stores own state. A failed attempt records error text and a next visible timestamp. After the maximum attempts are reached, the processor moves the row to dead-letter state.

Handlers and dispatchers must be idempotent. The inbox and outbox provide at-least-once behavior, not exactly-once side effects.

PostgreSQL Storage

PostgreSQL stores use raw Npgsql commands. Tables use jsonb payloads and FOR UPDATE SKIP LOCKED leasing. Schema helpers support migration-owned DDL (GetCreateScript, GetUpgradeScript), explicit bootstrap (EnsureAsync), opt-in host bootstrap, version metadata, advisory-locked upgrades, and startup validation. See PostgreSQL Schema Management.

Boundaries

  • LiteBus mediates in process. It does not own broker connections in core packages.
  • Inbox and outbox modules store commands and events, but workers decide when processors run.
  • Storage packages are optional and stay outside semantic modules.
  • DI adapters translate descriptors; mediator code does not depend on Microsoft DI or Autofac.

Clone this wiki locally