NashTech Blog

Saga Implementation with .NET 10 – Complete Guide

Table of Contents

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:

    1. Updates its own database

    1. Publishes an event

    1. Triggers the next step

    1. Provides a compensation action if something fails

Instead of one global transaction, we have eventual consistency.

Example: Order Processing

Steps:

    1. Create Order

    1. Reserve Inventory

    1. Process Payment

    1. 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.

Picture of truongtrinhv@nashtechglobal.com

truongtrinhv@nashtechglobal.com

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top