🧩 Saga Orchestration in .NET with CQRS, Event Sourcing, Hydration & Event Propagation
A beginner-friendly explanation
Modern distributed systems often need to coordinate long-running workflows such as order processing, payment authorization, inventory reservation, and shipping. These workflows involve multiple microservices communicating asynchronously. To manage this safely, we use Sagas.
This article explains how Sagas work in .NET and how they combine naturally with CQRS, Event Sourcing, and event propagation using tools like RabbitMQ and MassTransit.
⭐ 1. What is a Saga?
A Saga is a pattern for managing long-running, multi-step business processes that span multiple services.
A Saga helps you coordinate a workflow like:
- Create Order
- Reserve Inventory
- Process Payment
- Arrange Shipment
And it also defines compensation steps when something fails:
- If payment fails → release inventory
- If shipping fails → refund payment
A Saga ensures the full workflow completes successfully, or the system gracefully rolls back using compensating actions.
⭐ 2. How CQRS fits into a Saga
CQRS splits the system into two sides:
Write Side (Commands)
Each step of a Saga is triggered by a command:
ReserveInventoryCommandProcessPaymentCommandShipOrderCommand
Commands change state inside aggregates and generate events.
Read Side (Queries)
The read side builds projections of:
- Saga state (current step, status)
- Order status
- Payment status
This helps each service know what stage the workflow is in.
CQRS gives clarity and removes the need for shared databases between services.
⭐ 3. How Event Sourcing supports a Saga
With Event Sourcing, every state change is saved as an event:
OrderCreatedInventoryReservedPaymentAuthorizedShipmentScheduled
Each event becomes a reliable message that the Saga listens to.
The Saga then decides the next action based on the event.
For example:
OrderCreated → Start Saga → Send ReserveInventoryCommand
InventoryReserved → Send ProcessPaymentCommand
PaymentAuthorized → Send ShipOrderCommand
If anything fails:
PaymentFailed → Send ReleaseInventoryCommand → Mark Saga as Failed
⭐ 4. Hydration: rebuilding Saga state when needed
Because events are stored chronologically, the Saga can rebuild its state—this is called hydration.
When a Saga instance loads:
var events = eventStore.LoadStream(sagaId);
foreach (var e in events)
{
saga.Apply(e);
}
Hydration ensures:
- You don’t need to store complex Saga state in SQL
- The full workflow history is always available
- You can rebuild state anytime
⭐ 5. Event Propagation: how services communicate
After the write side stores events, they must be propagated so other services can react.
There are two types of propagation:
🔹 Internal propagation (inside same service)
- Use MediatR Notifications
- Update projections
- Trigger in-process handlers
🔹 External propagation (between microservices)
Use messaging systems such as:
- MassTransit (preferred in .NET; high-level abstraction)
- RabbitMQ
- Kafka
- Azure Service Bus
Flow:
- Aggregate emits events
- Event store saves them
- Event Publisher sends them to exchanges/topics
- Other services/processes receive and react
Example with RabbitMQ:
await bus.Publish(new InventoryReserved(orderId));
Example with MassTransit:
await _publishEndpoint.Publish(new PaymentAuthorized(orderId));
These messages trigger the next command in the Saga.
⭐ 6. Saga Orchestration Flow (Simple Example)
Let’s use a basic e-commerce workflow.
Step 1 — Order Created
- Saga starts
- Saga publishes
ReserveInventoryCommand
Step 2 — Inventory Reserved
- Saga publishes
ProcessPaymentCommand
Step 3 — Payment Authorized
- Saga publishes
ShipOrderCommand
If Something Fails
-
Saga sends compensating commands:
ReleaseInventoryCommandCancelOrderCommandRefundPaymentCommand
This ensures the system is always consistent.
⭐ 7. Saga State Machine (MassTransit Example)
MassTransit offers built-in Saga state machine features.
public class OrderState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
}
Saga state machine:
public class OrderSaga : MassTransitStateMachine<OrderState>
{
public State InventoryReserved { get; private set; }
public State PaymentCompleted { get; private set; }
public Event<OrderCreated> OrderCreated { get; private set; }
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Initially(
When(OrderCreated)
.Then(context => { /* start saga */ })
.TransitionTo(InventoryReserved)
.Publish(context => new ReserveInventoryCommand(context.Message.OrderId))
);
}
}
MassTransit handles:
- Saga persistence
- Message routing
- Correlation IDs
- Retries
- Error handling
⭐ 8. Why use Saga + CQRS + ES together?
| Feature | Benefit |
|---|---|
| Event Sourcing | Full history, hydration, audit trail |
| CQRS | Clean separation of writes/reads |
| Saga | Manage long workflows with compensation |
| RabbitMQ/MassTransit | Reliable async communication |
| Event Propagation | Each service reacts without tight coupling |
They form a strong pattern for distributed systems.
⭐ Final Summary
Saga orchestration becomes powerful when combined with CQRS and Event Sourcing:
- CQRS handles clear separation of commands and queries
- Event Sourcing makes state changes traceable and rebuildable
- Hydration recreates the saga state from events
- Event Propagation uses RabbitMQ or MassTransit to connect microservices
* Sagas coordinate multi-step workflows and ensure consistency through compensation
Sample GitHub Project

Top comments (0)