In modern distributed systems especially microservices architectures, maintaining data consistency across multiple services is one of the hardest problems to solve. Traditional ACID transactions don’t scale well across service boundaries, leading architects to adopt eventual consistency patterns instead.
One of the most widely used solutions is the Saga Pattern. This post focuses Complete Saga Implementation with Compensating Transactions
Complete Saga Orchestration Implementation Guide with Compensating Transactions
Getting Started
As systems evolve from monolithic architectures to distributed microservices, managing complex business workflows becomes increasingly challenging. Operations that were once handled within a single transactional boundary now span multiple services, each with its own data store and lifecycle. Ensuring consistency, reliability, and clear execution flow across these services requires a structured coordination mechanism.
The Orchestration Pattern addresses this challenge by introducing a centralized coordinator—known as the orchestrator—that manages and directs interactions between services. Rather than allowing services to communicate with each other in an ad hoc manner, the orchestrator explicitly defines the sequence of operations, invokes each service, and determines how the system should respond to successes or failures. This approach provides a clear, controlled workflow, making complex business processes easier to design, monitor, and maintain.
By separating workflow management from service implementation, the Orchestration Pattern enables better visibility, stronger error handling, and improved maintainability in distributed systems. It is especially valuable for long-running transactions and business processes where execution order, compensation, and reliability are critical.
Orchestration Pattern in .NET – Example
Below is a fully usable .NET 8 console application example implementing the Orchestration (Saga) Pattern with:
Steps:
- Create Order
- Reserve Inventory
- Process Payment
- Arrange Shipping
Full Working Code
Create a console application in .Net with name SagaOrchestrationDemo and replace Program.cs with the following:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SagaOrchestrationDemo
{
#region Contracts
public interface ISagaStep
{
string Name { get; }
Task ExecuteAsync(SagaContext context);
Task CompensateAsync(SagaContext context);
}
#endregion
#region Context
public class SagaContext
{
public Guid OrderId { get; set; }
public bool InventoryReserved { get; set; }
public bool PaymentProcessed { get; set; }
public bool ShippingArranged { get; set; }
}
#endregion
#region Orchestrator
public class SagaOrchestrator
{
private readonly IList<ISagaStep> _steps;
public SagaOrchestrator(IList<ISagaStep> steps)
{
_steps = steps;
}
public async Task ExecuteAsync(SagaContext context)
{
var completedSteps = new Stack<ISagaStep>();
try
{
foreach (var step in _steps)
{
Console.WriteLine($"\n➡ Executing: {step.Name}");
await step.ExecuteAsync(context);
completedSteps.Push(step);
}
Console.WriteLine("\n🎉 Saga completed successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ Error: {ex.Message}");
Console.WriteLine("🔁 Starting compensation...");
while (completedSteps.Count > 0)
{
var step = completedSteps.Pop();
Console.WriteLine($"↩ Compensating: {step.Name}");
await step.CompensateAsync(context);
}
Console.WriteLine("⚠ Saga rolled back.");
}
}
}
#endregion
#region Steps
public class CreateOrderStep : ISagaStep
{
public string Name => "Create Order";
public Task ExecuteAsync(SagaContext context)
{
context.OrderId = Guid.NewGuid();
Console.WriteLine($"Order created: {context.OrderId}");
return Task.CompletedTask;
}
public Task CompensateAsync(SagaContext context)
{
Console.WriteLine($"Order deleted: {context.OrderId}");
return Task.CompletedTask;
}
}
public class ReserveInventoryStep : ISagaStep
{
public string Name => "Reserve Inventory";
public Task ExecuteAsync(SagaContext context)
{
SimulateRandomFailure();
context.InventoryReserved = true;
Console.WriteLine("Inventory reserved.");
return Task.CompletedTask;
}
public Task CompensateAsync(SagaContext context)
{
if (context.InventoryReserved)
Console.WriteLine("Inventory released.");
return Task.CompletedTask;
}
private void SimulateRandomFailure()
{
if (Random.Shared.Next(0, 5) == 1)
throw new Exception("Inventory reservation failed!");
}
}
public class ProcessPaymentStep : ISagaStep
{
public string Name => "Process Payment";
public Task ExecuteAsync(SagaContext context)
{
SimulateRandomFailure();
context.PaymentProcessed = true;
Console.WriteLine("Payment processed.");
return Task.CompletedTask;
}
public Task CompensateAsync(SagaContext context)
{
if (context.PaymentProcessed)
Console.WriteLine("Payment refunded.");
return Task.CompletedTask;
}
private void SimulateRandomFailure()
{
if (Random.Shared.Next(0, 5) == 2)
throw new Exception("Payment processing failed!");
}
}
public class ArrangeShippingStep : ISagaStep
{
public string Name => "Arrange Shipping";
public Task ExecuteAsync(SagaContext context)
{
SimulateRandomFailure();
context.ShippingArranged = true;
Console.WriteLine("Shipping arranged.");
return Task.CompletedTask;
}
public Task CompensateAsync(SagaContext context)
{
if (context.ShippingArranged)
Console.WriteLine("Shipping canceled.");
return Task.CompletedTask;
}
private void SimulateRandomFailure()
{
if (Random.Shared.Next(0, 5) == 3)
throw new Exception("Shipping arrangement failed!");
}
}
#endregion
class Program
{
static async Task Main(string[] args)
{
var steps = new List<ISagaStep>
{
new CreateOrderStep(),
new ReserveInventoryStep(),
new ProcessPaymentStep(),
new ArrangeShippingStep()
};
var orchestrator = new SagaOrchestrator(steps);
var context = new SagaContext();
await orchestrator.ExecuteAsync(context);
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
}
}
}
How It Works
- ISagaStep → defines Execute + Compensate
- SagaOrchestrator → runs steps and handles rollback
- Each business step implements compensation logic
- Random failures simulate real-world errors
What This Demonstrates
- True Orchestration (central coordinator)
- Ordered execution
- Reverse-order compensation
- Isolation between steps
- Easy to extend with new steps
- Async-ready
Example Failure Scenario
If Payment fails, you’ll see: Order created
Inventory reserved
Error: Payment processing failed!
Starting compensation...
Compensating: Reserve Inventory
Inventory released
Compensating: Create Order
Order deleted
Saga rolled back.
Summary
The Saga Orchestration Pattern is a powerful solution for managing data consistency in distributed systems. By centralizing workflow control and handling failures through compensating actions, it enables scalable, resilient microservice architectures without relying on distributed transactions.
Thanks