PouryaKit
Concepts

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.

Feature code never saves, commits or touches the Outbox

No SaveChanges, no transaction, no Outbox write in a handler of any kind. The Unit of Work does each of them once, for the whole Operation, and structure tests reject a Module whose code does any of it.

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:

RenameProductCommandHandler.cs
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

GuaranteeStated in
One commit or nothingWhat the Kit guarantees
Saved with no save in Feature codeWhat the Kit guarantees
The Outbox in the commit, the send after itWhat the Kit guarantees
One version step per OperationVersions and conflicts
A refused save is an ErrorUnique values
No write without a Unit of WorkThe repository

Failure messages

Next steps

On this page