Distributed transactions are challenging in microservices since each service will have its own data store. The Saga pattern provides a means to handle transactions across services without a global transaction manager. Instead of an atomic transaction, a saga splits it into a sequence of local transactions with compensating transactions for failure.

In this post, I describe how you can implement the Saga pattern in C#, with real examples to demonstrate the flow.

Saga Concepts

A saga consists of.

Sagas can be dealt with in two basic ways:

Example Scenario: Order Processing Saga Consider an e-commerce process

If payment fails, we need to.

Orchestration Example (C#)

We'll utilize a basic saga orchestrator.

Saga Orchestrator

public class OrderSagaOrchestrator
{
    private readonly IOrderService _orderService;
    private readonly IInventoryService _inventoryService;
    private readonly IPaymentService _paymentService;

    public OrderSagaOrchestrator(
        IOrderService orderService,
        IInventoryService inventoryService,
        IPaymentService paymentService)
    {
        _orderService = orderService;
        _inventoryService = inventoryService;
        _paymentService = paymentService;
    }

    public async Task<bool> ProcessOrderAsync(OrderData order)
    {
        try
        {
            await _orderService.CreateOrderAsync(order);
            await _inventoryService.ReserveInventoryAsync(order.OrderId, order.Items);
            await _paymentService.ChargeAsync(order.OrderId, order.TotalAmount);
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Saga failed: {ex.Message}. Starting compensation...");
            await _paymentService.RefundAsync(order.OrderId);
            await _inventoryService.ReleaseInventoryAsync(order.OrderId);
            await _orderService.CancelOrderAsync(order.OrderId);
            return false;
        }
    }
}

Example Interfaces

public interface IOrderService
{
    Task CreateOrderAsync(OrderData order);
    Task CancelOrderAsync(string orderId);
}

public interface IInventoryService
{
    Task ReserveInventoryAsync(string orderId, List<Item> items);
    Task ReleaseInventoryAsync(string orderId);
}

public interface IPaymentService
{
    Task ChargeAsync(string orderId, decimal amount);
    Task RefundAsync(string orderId);
}

public class OrderData
{
    public string OrderId { get; set; }
    public List<Item> Items { get; set; }
    public decimal TotalAmount { get; set; }
}

public class Item
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
}

Choreography Example

In a choreography-based saga, all services listen to events. When, for example, the OrderCreated event is published, the Inventory Service hears it and reserves inventory.

Example RabbitMQ consumer for Inventory Service.

consumer.Received += async (model, ea) =>
{
    var message = Encoding.UTF8.GetString(ea.Body.ToArray());
    var orderCreated = JsonConvert.DeserializeObject<OrderCreatedEvent>(message);
    await _inventoryService.ReserveInventoryAsync(
        orderCreated.OrderId,
        orderCreated.Items
    );
    // Then publish InventoryReserved event
};

Best Practices

Conclusion

The Saga pattern enables your C# services to orchestrate complex workflows without distributed transactions. With orchestration or choreography, sagas ensure data consistency between services and handle failures elegantly. By using these principles carefully, you can create scalable and resilient distributed systems.