Choreography-Based Saga Pattern in Microservices

Here’s a full explanation of the Choreography-Based Saga Pattern, with a concrete C# (.NET) example using an event-driven approach.

Choreography-Based Saga Pattern in Microservices

Getting Started

In a microservices architecture, maintaining data consistency across services is challenging because traditional distributed transactions (2PC) don’t scale well and reduce system resilience.

The Saga Pattern solves this by breaking a business transaction into a series of local transactions, each managed by its own service. When something goes wrong, previously completed steps are undone using compensating transactions.

There are two Saga styles:
  • Orchestration (central coordinator)
  • Choreography (event-driven, decentralized)

Here we will focuses on choreography-based Saga Pattern, implemented using C# and .NET.

What Is Choreography-Based Saga?

In choreography:
  1. There is no central controller
  2. Each service:
    • Executes a local transaction
    • Publishes domain events
    • Reacts to events from other services
  3. Control flow emerges naturally from events
This improves decoupling and scalability but requires careful event and compensation design.

Example Business Scenario

We’ll model a simple Order Processing workflow:
  1. Order Service → Creates an order
  2. Payment Service → Charges the customer
  3. Inventory Service → Reserves stock
If any step fails, previous steps are compensated.

Demonstration

This demo illustrates a choreography-based Saga where an order is processed through a sequence of independent services(Order, Payment, and Inventory).

The Order service initiates the workflow by creating an order and publishing an event. The Payment service reacts to this event, processes the payment, and emits its own event upon success.

The Inventory service then attempts to reserve stock based on the payment confirmation. If the inventory reservation fails, a compensating action is triggered in which the Payment service refunds the previously completed payment. Throughout the entire process, coordination is achieved purely through events, with no central orchestrator controlling the flow, demonstrating an event-driven and loosely coupled Saga implementation.

Below is a complete, runnable .NET solution that demonstrates a choreography-based Saga using:
  1. .NET 7
  2. Console app
  3. In-memory event bus (no Kafka/RabbitMQ needed)
  4. Clear Saga flow + compensation
  5. Easy to extend later to MassTransit / Kafka
You can literally copy–paste this and run it.

Project Structure
SagaDemo/
├── Program.cs
├── EventBus.cs
├── Events.cs
├── OrderService.cs
├── PaymentService.cs
└── InventoryService.cs

Create the Project
Create a console project in Microsoft Visual Studio with C# language and add below classes to your project.
Event definitions (Events.cs)
namespace SagaDemo;
public interface IEvent
{
Guid SagaId { get; }
}
public record OrderCreated(Guid SagaId, Guid OrderId, decimal Amount) : IEvent;
public record PaymentSucceeded(Guid SagaId, Guid OrderId) : IEvent;
public record PaymentFailed(Guid SagaId, Guid OrderId, string Reason) : IEvent;
public record InventoryReserved(Guid SagaId, Guid OrderId) : IEvent;
public record InventoryFailed(Guid SagaId, Guid OrderId, string Reason) : IEvent;
Simple in-memory Event Bus (EventBus.cs)
namespace SagaDemo;
public interface IEventBus
{
  void Subscribe<T>(Func<T, Task> handler) where T : IEvent;
  Task PublishAsync<T>(T @event) where T : IEvent;
}
public class InMemoryEventBus : IEventBus
{
  private readonly Dictionary<Type, List<Func<IEvent, Task>>> _handlers = new();
  public void Subscribe<T>(Func<T, Task> handler) where T : IEvent
  {
    var eventType = typeof(T);
    if (!_handlers.ContainsKey(eventType))
    _handlers[eventType] = new List<Func<IEvent, Task>>();
    _handlers[eventType].Add(evt => handler((T)evt));
  }
  public async Task PublishAsync<T>(T @event) where T : IEvent
  {
    Console.WriteLine($"📢 Event Published: {typeof(T).Name}");
    var eventType = typeof(T);
    if (!_handlers.ContainsKey(eventType))
    return;
    foreach (var handler in _handlers[eventType])
    {
      await handler(@event);
    }
  }
}
Note:- This simulates Kafka / RabbitMQ.
Order Service (Saga starter)
namespace SagaDemo;
public class OrderService
{
  private readonly IEventBus _bus;
  public OrderService(IEventBus bus)
  {
    _bus = bus;
  }
  public async Task CreateOrder(Guid orderId, decimal amount)
  {
    var sagaId = Guid.NewGuid();
    Console.WriteLine($"📝 Order created: {orderId}");
    await _bus.PublishAsync(
    new OrderCreated(sagaId, orderId, amount));
  }
}
Payment Service (with compensation)
namespace SagaDemo;
public class PaymentService
{
  private readonly IEventBus _bus;
  public PaymentService(IEventBus bus)
  {
    _bus = bus;
    bus.Subscribe<OrderCreated>(Handle);
    bus.Subscribe<InventoryFailed>(Handle);
  }
  private async Task Handle(OrderCreated evt)
  {
    Console.WriteLine($"💳 Charging payment for Order {evt.OrderId}");
    // Simulate success
    await Task.Delay(300);
    await _bus.PublishAsync(
    new PaymentSucceeded(evt.SagaId, evt.OrderId));
  }
  private async Task Handle(InventoryFailed evt)
  {
    Console.WriteLine($"↩️ Refunding payment for Order {evt.OrderId}");
    await Task.Delay(200);
  }
}
Inventory Service (failure triggers compensation)
namespace SagaDemo;
public class InventoryService
{
  private readonly IEventBus _bus;
  public InventoryService(IEventBus bus)
  {
    _bus = bus;
    bus.Subscribe<PaymentSucceeded>(Handle);
  }
  private async Task Handle(PaymentSucceeded evt)
  {
    Console.WriteLine($"📦 Reserving inventory for Order {evt.OrderId}");
    await Task.Delay(300);
    // 🔥 Simulate inventory failure
    var success = false;
    if (success)
    {
      await _bus.PublishAsync(
      new InventoryReserved(evt.SagaId, evt.OrderId));
    }
    else
    {
      await _bus.PublishAsync(
      new InventoryFailed(evt.SagaId, evt.OrderId, "Out of stock"));
    }
  }
}
Program.cs (Wire everything together)
using SagaDemo;
var bus = new InMemoryEventBus();
var orderService = new OrderService(bus);
var paymentService = new PaymentService(bus);
var inventoryService = new InventoryService(bus);
Console.WriteLine("🚀 Starting Saga...\n");
await orderService.CreateOrder(
orderId: Guid.NewGuid(),
amount: 100m);
Console.WriteLine("\n✅ Saga finished. Press any key.");
Console.ReadKey();

Run the it and the output will be like this
🚀 Starting Saga...
📝 Order created: 9c92...
📢 Event Published: OrderCreated
💳 Charging payment for Order 9c92...
📢 Event Published: PaymentSucceeded
📦 Reserving inventory for Order 9c92...
📢 Event Published: InventoryFailed
↩️ Refunding payment for Order 9c92...
✅ Saga finished. Press any key.

Summary

Choreography-based Saga is a powerful pattern for building resilient, scalable microservices using event-driven communication. With careful event design, idempotency, and compensation handling, it enables consistency without distributed transactions.

Thanks

Kailash Chandra Behera

I am an IT professional with over 13 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال