Thursday, November 20, 2025

Architecture - Saga Design Pattern

"Saga" is a distributed transaction pattern used in microservices to ensure data consistency without using 2-phase commit or distributed locks.

✔ In simpler words:

Saga ensures that a long-running business process composed of multiple microservice operations either completes successfully OR compensates (undoes) the already completed steps.


 

๐Ÿš€ Use Saga when:

  • A single business operation touches multiple microservices, each with its own DB.
  • You cannot use distributed transactions (like ACID/2PC).
  • You need an eventual consistency model.

๐ŸŽฏ Why Saga Is Needed

Typical distributed system problems:

  • Every service owns its database → no cross-service transaction possible.
  • Operations may fail in the middle.
  • You must prevent partial success (e.g., payment done but inventory not reserved).

Traditional ACID transactions do not work across services—Saga solves this using compensating actions.


๐Ÿง  Saga Pattern Key Principles

  1. A business workflow is broken into multiple local transactions.
  2. Each local transaction:
    • Performs a step.
    • Publishes an event.
  3. If any step fails → Saga triggers compensation (undo logic).

๐Ÿ”„ Two Types of Saga: Orchestration vs Choreography


๐Ÿ…ฐ️ 1. Choreography-Based Saga (Event-Driven)

Services talk to each other using events.


✔ Flow example:

  1. Order Service → "OrderCreated" event
  2. Inventory Service → reserves stock → "StockReserved"
  3. Payment Service → charges customer → "PaymentSuccess"
  4. Shipping Service → ships product → "OrderShipped"

If failure:

  • Payment fails → publish "PaymentFailed"
  • Inventory Service → receives and releases reserved stock
  • Order Service → marks order as cancelled

✔ Pros

  • No central coordinator
  • Simple for small flows
  • Very loosely coupled

❌ Cons

  • Hard to visualize / debug
  • Complex chains of events → “distributed spaghetti
  • Hard to evolve as workflow grows

๐Ÿ…ฑ️ 2. Orchestration-Based Saga

A central Saga Orchestrator commands each step.


✔ Flow example:

  1. Orchestrator → ReserveInventory()
  2. Orchestrator → ChargePayment()
  3. Orchestrator → CreateShipment()

If failure:

  • Orchestrator calls CompensateInventory(), RefundPayment(), etc.

✔ Pros

  • Centralized logic → easier to understand
  • Traceable
  • Easier for complex workflows

❌ Cons

  • Orchestrator becomes the “brain” → slightly more coupling


๐Ÿ“ฆ Real Example: E-Commerce Order Workflow

Step-by-step:

  1. Order Service
    • Creates order
    • Saga starts
  2. Inventory Service
    • Reserves stock
  3. Payment Service
    • Charges customer
  4. Shipping Service
    • Schedules delivery
  5. Saga completes

If Payment fails:

Saga automatically:

  • Cancels shipment
  • Releases stock
  • Cancels order

๐Ÿงฐ C# Example — Orchestration Saga Using MassTransit

public class OrderState : SagaStateMachineInstance { public Guid CorrelationId { get; set; } public string CurrentState { get; set; } public Guid OrderId { get; set; } } public class OrderStateMachine : MassTransitStateMachine<OrderState> { public State InventoryReserved { get; private set; } public Event<OrderCreated> OrderCreatedEvent { get; private set; } public OrderStateMachine() { InstanceState(x => x.CurrentState); Event(() => OrderCreatedEvent, x => x.CorrelateById(context => context.Message.OrderId)); Initially( When(OrderCreatedEvent) .Then(context => { Console.WriteLine("Order Created → Reserving inventory..."); }) .Publish(context => new ReserveInventory { OrderId = context.Instance.OrderId }) .TransitionTo(InventoryReserved) ); // Additional states + transitions omitted for brevity } }

This orchestrates:

  • Reserve inventory
  • Process payment
  • Ship order
  • Handle compensation

๐Ÿงฐ C# Example — Choreography Saga (Event-Driven)

Order Service publishes event:

await _publishEndpoint.Publish(new OrderCreated(orderId));

Inventory handles event:

public class OrderCreatedHandler : IConsumer<OrderCreated> { public async Task Consume(ConsumeContext<OrderCreated> context) { // Local transaction ReserveStock(context.Message.OrderId); await context.Publish(new StockReserved { OrderId = context.Message.OrderId }); } }

Payment handles event:

public class StockReservedHandler : IConsumer<StockReserved> { public async Task Consume(ConsumeContext<StockReserved> context) { var result = Charge(context.Message.OrderId); if (result) await context.Publish(new PaymentCompleted { OrderId = context.Message.OrderId }); else await context.Publish(new PaymentFailed { OrderId = context.Message.OrderId }); } }

๐Ÿงจ Compensating Transaction Example

public class PaymentFailedHandler : IConsumer<PaymentFailed> { public async Task Consume(ConsumeContext<PaymentFailed> context) { await context.Publish(new ReleaseStock { OrderId = context.Message.OrderId }); await context.Publish(new CancelOrder { OrderId = context.Message.OrderId }); } }

๐ŸŽฏ When to Use Saga

Use Saga when:

  • Business processes affect multiple services.
  • Rollbacks must be handled safely.
  • Workflow is long-running (minutes → hours).
  • Consistency between services is required.

Don't use Saga when:

  • Single service owns all data.
  • Strong ACID guarantees needed.
  • Steps cannot be compensated.

No comments:

Post a Comment

CI/CD - Safe DB Changes/Migrations

Safe DB Migrations means updating your database schema without breaking the running application and without downtime . In real systems (A...