Transaction Management via Global LiteBus Pipeline Behaviors
Status: Accepted
Date: 2026-01-01
Context
Command handlers need database transactions to ensure atomicity. Without a transaction, a command that modifies an aggregate and writes to a secondary table (for example, an outbox table) can leave the system in a partially updated state if the second write fails.
The options evaluated were:
- Explicit
IUnitOfWorkinterface injected into every command handler. Handlers callSaveChangesAsyncexplicitly. Maximum explicitness, maximum boilerplate. - A LiteBus pipeline post-handler that calls
SaveChangesAsyncafter every command. Handlers contain no persistence code. The pipeline handles it.
Option 2 was chosen. The rationale: transaction management is not business logic. It is infrastructure ceremony that belongs in infrastructure, not in every command handler. The same reasoning applies to GlobalExceptionHandler removing try-catch from every endpoint: cross-cutting infrastructure concerns belong in a central location.
LiteBus supports global handlers typed against ICommand that run for every command via polymorphic dispatch. This covers both ICommand (void) and ICommand<TResult> (with result).
Three global handlers are registered in Infrastructure:
TransactionCommandPreHandler : ICommandPreHandler<ICommand>at priority 10. Opens a database transaction. Runs after validators (priority 0) so no transaction is opened for invalid input.SaveChangesCommandPostHandler : ICommandPostHandler<ICommand>. CallsSaveChangesAsyncand commits the transaction after the command handler succeeds.RollbackCommandErrorHandler : ICommandErrorHandler<ICommand>. Rolls back the transaction on any exception and re-throws.
The outbox pattern (docs/decisions/outbox-pattern-as-reliability-escalation.md) integrates into SaveChangesCommandPostHandler: before calling SaveChangesAsync, the post-handler collects domain events from tracked aggregates and writes them to the outbox table in the same transaction. Command handlers never change regardless of whether the outbox is active.
Decision
Transaction management is handled by three global LiteBus pipeline behaviors registered in Infrastructure. Command handlers do not call SaveChangesAsync. Repositories do not call SaveChangesAsync. The pipeline handles all persistence.
Consequences
Positive
- Command handlers contain only business logic. No persistence ceremony.
- Repositories stage changes without committing. They are pure domain-operation implementations.
- The outbox pattern integrates in one place without touching any command handler.
- Adding a new command requires zero transaction-related code.
Negative
- A developer reading a command handler cannot see where
SaveChangesAsyncis called. The behavior is in the pipeline. - Debugging transaction issues requires understanding the pipeline, not just the handler.
Risks
- A command that intentionally should not open a transaction (for example, a read-only side-effect command) would have a transaction opened unnecessarily. Use
[HandlerTag]to exclude specific commands from the global pipeline behaviors when needed.