Saga
A Module's long-running reaction across Integration Events that keeps its own state between steps, and the one reaction that runs outside an Operation.
What it is
A Saga is a Module's long-running reaction across Integration Events that keeps its own state between steps. Integration Events start and advance it, its own timeouts end it, and each step can emit Integration Events of its own. A saga's identity is the tenant of the event that started it with that event's business id.
A Saga is the one reaction that runs outside an Operation. Each step runs with no Operation Context, is claimed once in its own transaction, and emits its Integration Events under the saga's system Actor.
A step only changes the saga's state, emits and schedules
No authorizer, validator, Unit of Work or Receipt runs for a step. A step that dies before its commit runs again, so a call to another service or a file it wrote would repeat. Anything else is the work of a subscription that receives what the saga emits.
When to use it
When a reaction waits for several Integration Events, keeps what it has seen between them, and gives up after a time. In the Kit's tests, a fulfilment saga starts when an order is placed, counts its payments, and either fulfils the order once all payments arrived or cancels it when they did not arrive in time. A reaction to one Integration Event at a time is an Integration Event Handler: see Integration Events.
How it works
A Module declares its saga in its Composition Root: its name and system Actor, the Integration Events that start or advance it with the business id each one names, and its timeouts.
module.AddPouryaKitSaga<FulfilmentSaga>(
Fulfilment,
systemActor: Fulfilment,
saga =>
saga.Input<OrderPlacedV1>(placed => placed.OrderId)
.Input<PaymentReceivedV1>(paid => paid.OrderId)
.Input<PaymentReceivedV2>(paid => paid.OrderId)
.Timeout<FulfilmentTimeout>()
);The saga derives from PouryaKitSaga. A starting step, named Start, returns the new saga and the timeouts it schedules:
/// <summary>Starts the saga, and schedules its timeout.</summary>
public static (FulfilmentSaga, FulfilmentTimeout) Start(
SagaInput<FulfilmentSaga, OrderPlacedV1> placed
) =>
(
new FulfilmentSaga
{
Order = placed.Event.OrderId,
Installments = placed.Event.Installments,
},
new FulfilmentTimeout(TimeSpan.FromSeconds(placed.Event.PayWithinSeconds))
);An advancing step, named Handle, takes a later event or a timeout. It emits with PublishAsync and completes the saga with MarkCompleted:
/// <summary>The payments did not arrive in time.</summary>
public async Task Handle(FulfilmentTimeout timeout)
{
await PublishAsync(new OrderCancelledV1(Order, Paid));
MarkCompleted();
}The Host runs sagas on Wolverine's saga storage in its PostgreSQL message store, with UsePouryaKitWolverine. Outside Development the Host loads each saga's generated code, which the step before a release writes: see Prepare a release.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| A route of its own, one claim per step, the tenant in the identity, emissions an ordinary subscription receives | What the Kit guarantees |
| What a step must not do | What a step must not do |
| What fails the Host when it is built or started | The Host |
| What a saga cannot do yet | Limits |
Failure messages
Next steps
Stream
An aggregate whose stored state is the events it appended, the Kit's second write model, which commits through its own Unit of Work on Marten.
Gateway tools and MCP tools
A Gateway tool runs a model's call as an Operation through a Module's Gateway; an MCP tool runs on its server and is no Operation.