C#  

Saga Pattern in C#: Reliable Transaction Orchestration Example

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:

  • Orchestration: A central coordinator (Saga Orchestrator) controls the workflow.

  • Choreography: Each service reacts to events from the previous one.

🧩 Saga Pattern Flow Example

Let’s take a simple example: a file upload operation consisting of three steps:

  1. Prepare database entry

  2. Upload file to Azure Blob

  3. Finalize transaction

When any step fails

  • The orchestrator calls the compensating operation for previously successful steps.

  • The database and blobs return to a consistent state.

⚙️ 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

FeatureDescription
ReliabilityEnsures system consistency even when partial steps fail.
DecouplingEach step manages its own transaction logic.
ScalabilitySuitable for microservice environments with asynchronous steps.
ObservabilityEasy 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.