Unit of Work
The one owner of an Operation's outcome, which commits everything it changed together or discards it together.
What it is
The Unit of Work is the single owner of an Operation's outcome: everything the Operation changed is kept together or discarded together. Each command runs in one transaction that the Unit of Work begins and owns. Your handler changes aggregates through a repository and returns its Result; the Unit of Work does everything after that.
When it matters
Every command. You never call the Unit of Work yourself: the Module's persistence package registers it when the Composition Root declares the Module's data, for example with UsePouryaKitPostgres, and the Kit runs it around each command. A query starts none of its own.
How it works
After the outermost handler returns a success, the Unit of Work finishes the Operation in a fixed order. It runs the Domain Event Handlers of the Domain Events the Operation's aggregates raised, prepares what it saves, builds the Snapshots other Modules subscribe to and writes the Outbox, then saves and commits once. The Unit of Work lists each step, and Outbox and Notifications explains what happens to what it wrote.
A failed Result anywhere, a nested command's or a Domain Event Handler's included, rolls all of it back. This handler changes a product and saves nothing; the Unit of Work commits the change:
internal sealed class RenameProductCommandHandler(IRepository<Product, ProductId> products)
: CommandHandler<RenameProductCommand>
{
public override async Task<Result> Handle(
RenameProductCommand command,
CancellationToken cancellationToken
)
{
var product = await products.FindAsync(new ProductId(command.ProductId), cancellationToken);
return product is null ? ProductErrors.NotFound : product.Rename(command.Name);
}
}An aggregate's version guards it against a concurrent change. When another Operation committed a change to the same aggregate first, the save is refused and the Operation fails with a conflict Error. Nothing retries it: the caller sends the command again through the Gateway, which runs a new Operation on the current state. See Versions and conflicts.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| One commit or nothing | What the Kit guarantees |
| Saved with no save in Feature code | What the Kit guarantees |
| The Outbox in the commit, the send after it | What the Kit guarantees |
| One version step per Operation | Versions and conflicts |
| A refused save is an Error | Unique values |
| No write without a Unit of Work | The repository |