The Saga Pattern is used to manage long-running or distributed transactions where a single atomic transaction isn’t possible — for example, when coordinating operations across multiple microservices or databases and external resources like Azure Blob Storage.
Use Case: When a process involves multiple steps (e.g., reserving inventory, charging a payment, and sending an order confirmation), and you need all or none of the steps to succeed.
💡 What is the Saga Pattern?
A Saga is a sequence of local transactions. Each transaction updates data within a single service and publishes an event or triggers the next step. If a step fails, the system executes compensating transactions to undo the previous successful steps.
There are two main approaches:
🧩 Saga Pattern Flow Example
Let’s take a simple example: a file upload operation consisting of three steps:
Prepare database entry
Upload file to Azure Blob
Finalize transaction
When any step fails
⚙️ Implementation in C# (Orchestrator Pattern)
Below is a simple demonstration of the Saga Orchestrator in C# using .NET and EF Core concepts.
1️⃣ Saga Step Interface
public interface ISagaStep
{
Task ExecuteAsync();
Task CompensateAsync();
}
2️⃣ Define Steps
// Step 1 - Prepare database record
public class PrepareDbStep : ISagaStep
{
private readonly AppDbContext _db;
private readonly Guid _transactionId;
public PrepareDbStep(AppDbContext db, Guid transactionId)
{
_db = db;
_transactionId = transactionId;
}
public async Task ExecuteAsync()
{
_db.FileTransfers.Add(new FileTransfer
{
TransactionId = _transactionId,
Status = "Preparing",
CreatedOn = DateTime.UtcNow
});
await _db.SaveChangesAsync();
Console.WriteLine("✅ DB preparation done");
}
public async Task CompensateAsync()
{
var tx = await _db.FileTransfers.FindAsync(_transactionId);
if (tx != null)
{
_db.FileTransfers.Remove(tx);
await _db.SaveChangesAsync();
Console.WriteLine("↩️ DB preparation reverted");
}
}
}
// Step 2 - Upload file to Azure Blob
public class UploadBlobStep : ISagaStep
{
private readonly BlobContainerClient _container;
private readonly string _filePath;
private string _blobName;
public UploadBlobStep(BlobContainerClient container, string filePath)
{
_container = container;
_filePath = filePath;
}
public async Task ExecuteAsync()
{
_blobName = Path.GetFileName(_filePath);
var blob = _container.GetBlobClient(_blobName);
await blob.UploadAsync(_filePath, overwrite: true);
Console.WriteLine($"✅ Uploaded {_blobName}");
}
public async Task CompensateAsync()
{
var blob = _container.GetBlobClient(_blobName);
await blob.DeleteIfExistsAsync();
Console.WriteLine($"↩️ Deleted blob {_blobName}");
}
}
// Step 3 - Mark DB completed
public class CompleteDbStep : ISagaStep
{
private readonly AppDbContext _db;
private readonly Guid _transactionId;
public CompleteDbStep(AppDbContext db, Guid transactionId)
{
_db = db;
_transactionId = transactionId;
}
public async Task ExecuteAsync()
{
var tx = await _db.FileTransfers.FindAsync(_transactionId);
if (tx != null)
{
tx.Status = "Completed";
tx.UpdatedOn = DateTime.UtcNow;
await _db.SaveChangesAsync();
Console.WriteLine("✅ Marked transaction completed");
}
}
public Task CompensateAsync() => Task.CompletedTask; // nothing to revert here
}
3️⃣ Saga Orchestrator
public class SagaOrchestrator
{
private readonly List<ISagaStep> _steps = new();
private readonly List<ISagaStep> _executed = new();
public SagaOrchestrator AddStep(ISagaStep step)
{
_steps.Add(step);
return this;
}
public async Task ExecuteAsync()
{
foreach (var step in _steps)
{
try
{
await step.ExecuteAsync();
_executed.Add(step);
}
catch (Exception ex)
{
Console.WriteLine($"❌ Step failed: {ex.Message}");
await CompensateAsync();
throw;
}
}
}
private async Task CompensateAsync()
{
foreach (var step in _executed.AsEnumerable().Reverse())
{
try { await step.CompensateAsync(); }
catch (Exception ex) { Console.WriteLine($"⚠️ Compensation failed: {ex.Message}"); }
}
}
}
4️⃣ Using the Saga
public class FileTransferService
{
private readonly AppDbContext _db;
private readonly BlobContainerClient _blob;
public FileTransferService(AppDbContext db, BlobContainerClient blob)
{
_db = db;
_blob = blob;
}
public async Task RunSagaAsync(string localFile)
{
var txId = Guid.NewGuid();
var saga = new SagaOrchestrator()
.AddStep(new PrepareDbStep(_db, txId))
.AddStep(new UploadBlobStep(_blob, localFile))
.AddStep(new CompleteDbStep(_db, txId));
try
{
await saga.ExecuteAsync();
Console.WriteLine("🎉 Saga completed successfully");
}
catch
{
Console.WriteLine("💥 Saga failed, all changes reverted");
}
}
}
🧠 How It Works
Each step executes independently. If any step throws an exception, the orchestrator calls all completed steps’ CompensateAsync() in reverse order, restoring system consistency.
📊 Benefits
| Feature | Description |
|---|
| Reliability | Ensures system consistency even when partial steps fail. |
| Decoupling | Each step manages its own transaction logic. |
| Scalability | Suitable for microservice environments with asynchronous steps. |
| Observability | Easy to trace and audit the progress of distributed workflows. |
⚠️ Best Practices
Implement idempotent operations so retries don’t cause duplicate effects.
Always persist the saga state (transaction ID, step status) for resilience.
Use a message queue (e.g., Azure Service Bus, RabbitMQ) for asynchronous saga orchestration.
Implement a compensation retry policy in case of partial rollback failures.
Author’s Note: The Saga pattern is the cornerstone of reliability in distributed .NET systems. It allows you to safely combine database and external API operations while preserving consistency.