Introduction
In modern distributed systems, data consistency across multiple services is one of the biggest challenges. Traditional ACID transactions do not scale well in microservice architectures. This is where the Saga pattern becomes essential.
This article explains:
-
- What Saga is and why it matters
-
- Saga orchestration vs choreography
-
- Implementing Saga using .NET 10
-
- Real‑world code example with MassTransit + RabbitMQ
-
- Best practices for production systems
1. What is the Saga Pattern?
A Saga is a sequence of local transactions where each step:
-
- Updates its own database
-
- Publishes an event
-
- Triggers the next step
-
- Provides a compensation action if something fails
Instead of one global transaction, we have eventual consistency.
Example: Order Processing
Steps:
-
- Create Order
-
- Reserve Inventory
-
- Process Payment
-
- Ship Order
If payment fails → release inventory + cancel order.
2. Saga Styles
2.1 Choreography
-
- Services communicate via events
-
- No central coordinator
-
- Simple but hard to debug
2.2 Orchestration
-
- Central Saga orchestrator controls workflow
-
- Easier monitoring and error handling
-
- Preferred for complex business flows
3. Why .NET 10 for Saga?
.NET 10 brings:
-
- Improved performance & async pipelines
-
- Better minimal APIs & background processing
-
- Native support for OpenTelemetry
-
- Strong ecosystem: MassTransit, NServiceBus, Dapr
4. Architecture Overview
Typical stack:
-
- API Gateway – receives requests
-
- Order Service – starts Saga
-
- Inventory Service – reserves stock
-
- Payment Service – charges customer
-
- Shipping Service – ships order
-
- Message Broker – RabbitMQ / Kafka
- Saga State Store – database or Redis
5. Implementing Saga with MassTransit in .NET 10
5.1 Install Packages
dotnet add package MassTransit
dotnet add package MassTransit.RabbitMQ
5.2 Define Messages
public record OrderCreated(Guid OrderId, decimal Amount);
public record InventoryReserved(Guid OrderId);
public record PaymentProcessed(Guid OrderId);
public record PaymentFailed(Guid OrderId, string Reason);
5.3 Create Saga State
using MassTransit;
public class OrderSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = default!;
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
}
5.4 Create Saga State Machine
using MassTransit;
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
public State AwaitingInventory { get; private set; } = default!;
public State AwaitingPayment { get; private set; } = default!;
public Event<OrderCreated> OrderCreated { get; private set; } = default!;
public Event<InventoryReserved> InventoryReserved { get; private set; } = default!;
public Event<PaymentProcessed> PaymentProcessed { get; private set; } = default!;
public Event<PaymentFailed> PaymentFailed { get; private set; } = default!;
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Event(() => OrderCreated, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => InventoryReserved, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentProcessed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentFailed, x => x.CorrelateById(m => m.Message.OrderId));
Initially(
When(OrderCreated)
.Then(ctx =>
{
ctx.Saga.OrderId = ctx.Message.OrderId;
ctx.Saga.Amount = ctx.Message.Amount;
})
.TransitionTo(AwaitingInventory)
.Publish(ctx => new ReserveInventory(ctx.Saga.OrderId))
);
During(AwaitingInventory,
When(InventoryReserved)
.TransitionTo(AwaitingPayment)
.Publish(ctx => new ProcessPayment(ctx.Saga.OrderId, ctx.Saga.Amount))
);
During(AwaitingPayment,
When(PaymentProcessed)
.Finalize(),
When(PaymentFailed)
.Publish(ctx => new ReleaseInventory(ctx.Saga.OrderId))
.Finalize()
);
SetCompletedWhenFinalized();
}
}
5.5 Configure in Program.cs
builder.Services.AddMassTransit(x =>
{
x.AddSagaStateMachine<OrderSaga, OrderSagaState>()
.InMemoryRepository();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("localhost");
cfg.ConfigureEndpoints(context);
});
});
6. Compensation Logic
Compensation is critical for data consistency.
Example:
When(PaymentFailed)
.Publish(ctx => new ReleaseInventory(ctx.Saga.OrderId))
.Publish(ctx => new CancelOrder(ctx.Saga.OrderId));
7. Production Best Practices
Reliability
-
- Use persistent saga storage (SQL, MongoDB)
-
- Enable retry + circuit breaker
-
- Ensure idempotent handlers
Observability
-
- Integrate OpenTelemetry + Jaeger
-
- Log correlation IDs
Scalability
-
- Prefer event‑driven communication
- Use outbox pattern to avoid message loss
8. Common Pitfalls
-
- Missing compensation steps
-
- Non‑idempotent consumers
-
- Large saga state objects
-
- Tight coupling between services
9. Conclusion
The Saga pattern is the foundation of reliable microservices. With .NET 10 and tools like MassTransit, implementing distributed workflows becomes clean, scalable, and production‑ready.