Saga Pattern in Microservices

Saga Pattern in ASP.NET Core Web API Microservices

The Saga Pattern is a design pattern for handling Distributed Transactions across multiple microservices. In a microservices world, each service owns its own database. This makes it very difficult to run a single ACID transaction across multiple services.

Instead of relying on global transactions, Saga breaks the business workflow into a series of local transactions. Each local transaction updates its own service/database and then triggers the next step, either by publishing an event (Choreography) or by responding to a coordinator (Orchestration). If something fails, Saga uses compensating transactions to undo previously successful steps, ensuring the system returns to a consistent state.

  • A Saga is a Sequence of Local Transactions across multiple services.
  • Each service performs its Own Atomic Local Transaction and then triggers the Next Step.
  • If a step fails, Saga triggers Compensating Transactions to Rollback Business Actions, not just database changes.

Why Do We Need the Saga Pattern?

In a Microservices Architecture, every service owns its own database (Decentralized Data Management). This makes it impossible to use one global ACID transaction across services. Without Saga, failures can easily lead to inconsistent states where some services succeed, but others fail.

Consider the case of placing an order in an e-commerce system:

  • Order Service → Creates the order.
  • Payment Service → Charges the customer.
  • Product Service → Reduces stock in inventory.
  • Notification Service → Sends order confirmation.

Now, imagine Payment succeeds, but Product stock update fails:

  • The customer is charged, but the product is not reserved.
  • This leaves the system inconsistent, resulting in a poor customer experience.

The Saga Pattern ensures the system is always returned to a consistent state, even in the face of failures.

  • All steps succeed:
      • The order is confirmed, payment is successful, stock is reserved, and the notification is sent.
  • If any step fails:
      • Compensating transactions undo previous steps. For example:
          • Cancel the order if payment fails.
          • Refund payment if the inventory reservation fails.

The Saga Pattern provides three important guarantees:

  • Ensuring Data Consistency across multiple services without using distributed transactions.
  • Providing Rollback Mechanisms via compensating transactions rather than traditional database rollbacks.
  • Supporting Long-Lived Business Processes using eventual consistency, retries, and compensation logic.

Types of Saga Pattern

There are two main types of Saga implementation:

Choreography (Event-Driven Saga)

No central coordinator; services communicate through events. Each service subscribes to events, performs its local transaction, and emits a new event. Compensation is triggered by failure events.

  1. If a service fails, it emits a failure event, and subscribers react with their compensation logic.
  2. This results in a decentralized, loosely coupled architecture that scales well, but is harder to trace.
Example:
  1. Order Service → emits OrderPlacedEvent.
  2. Payment Service → listens, processes payment, emits PaymentCompletedEvent or PaymentFailedEvent.
  3. Product Service → listens, reserves stock, emits InventoryReservedEvent or InventoryFailedEvent.
  4. Notification Service → listens, sends confirmation or failure messages.
Orchestration (Centralized Saga Coordinator)

A central component (orchestrator) manages the workflow. It tells each service what to do next and listens for responses. On failure, it commands compensating transactions in the right order.

  1. Based on responses (success/failure), it decides the next step or invokes compensation.
  2. The entire transaction’s logic is centralized, making it easier to monitor and debug.
Example:
  1. Orchestrator → tells Order Service to create an order.
  2. Orchestrator → tells Payment Service to charge the customer.
  3. Orchestrator → tells Product Service to reserve stock.
  4. Orchestrator → tells Notification Service to send confirmation.
  5. If the stock reservation fails, the orchestrator instructs the Payment Service to issue a refund and the Order Service to cancel the order.

Real-Time Example: Order Placement in Our E-Commerce System

Let us see one realistic scenario where the Saga Pattern is ideal. Placing an order in the e-commerce system touches multiple microservices:

  • OrderService → Business entry point.
  • PaymentService → Financial transaction.
  • ProductService → Stock reservation.
  • NotificationService → Customer updates.

The Saga Pattern provides a robust mechanism for managing distributed transactions in microservices by combining local transactions with compensating actions. Whether implemented through Choreography or Orchestration, the pattern ensures that long-running business processes remain consistent and reliable, even in the face of partial failures.

Implementing Saga Pattern using Orchestrator

What is Saga with Orchestrator?

Saga Orchestration means there is a Central Coordinator (Orchestrator Service) that manages the sequence of steps in a business transaction. Instead of each service reacting to events independently (Choreography), the Orchestrator controls the workflow:

  1. Calls next service
  2. Decides what happens if something fails
  3. Publishes compensating actions

The Orchestrator is a separate microservice that coordinates distributed transactions. It keeps the workflow logic in one place, so individual services (Order, Product, Notification) stay focused on their own business logic.

Example Scenario:

We will implement the Saga Pattern with Orchestration with the following flow:

  • OrderService places an order → saves to DB → publishes OrderPlacedEvent.
  • OrchestratorService consumes OrderPlacedEvent → asks ProductService to reserve stock.
  • ProductService checks inventory → publishes StockReservedEvent or StockReservationFailedEvent.
  • OrchestratorService consumes result → publishes either OrderConfirmedEvent or OrderCancelledEvent.
  • OrderService consumes OrderCancelledEvent → compensates (sets status Cancelled).
  • NotificationService consumes OrderConfirmedEvent/OrderCancelledEvent → notifies customer.

Note: No direct communication between the services; communication only via the Orchestrator Service.

Step 1: Define Shared Messaging Infrastructure in Messaging.Common

This is the foundation of your Saga pattern. All services reference it, ensuring a single source of truth for contracts and RabbitMQ setup. Every service needs to speak the same “language” when exchanging events and Messaging.Common is the shared dictionary.

Models Folder:

Shared helper models that multiple events reuse.

  • EventBase → ensures all events have a unique ID, timestamp, and correlation ID for tracing.
  • OrderItemLine → Represents a single product line in an order.
  • FailedLine → Represents a single product line for a failed order with the reason.
Events Folder:

These classes are your contracts. They define what data moves between services.

  • OrderPlacedEvent → Raised by OrderService when a new order is created.
  • StockReservedEvent → Raised by ProductService if stock deduction succeeds.
  • StockReservationFailedEvent → Raised by ProductService if stock is insufficient.
  • OrderConfirmedEvent → Raised by Orchestrator if stock is reserved successfully.
  • OrderCancelledEvent → Raised by Orchestrator if stock reservation failed.
Options Folder:

Defines exchange, queues, and DLX (dead-letter exchange). Services bind their queues using these options to ensure consistent routing.

  • RabbitMqOptions → central place for connection details + queue/exchange names. Each microservice loads its own appsettings.json, but the format is identical.
Topology Folder:

Central place to declare exchanges, queues, bindings, and DLX. Ensures all microservices use the same queue/exchange definitions, avoiding mismatches in their names.

  • RabbitTopology → defines exchanges, DLX, and queue bindings. Ensures all services agree on routing (e.g., order.placed goes to orchestrator + product).
Connection Folder:

Keeps a single long-lived RabbitMQ connection per service, rather than having each publisher/consumer create a new one.

  • ConnectionManager → manages RabbitMQ connection lifecycle (one per service).
Consuming Folder:

Abstract class that handles JSON deserialization, Ack/Nack, and error handling. Each service only implements HandleMessage(T message, string correlationId). This enforces a standard consumer style across services.

  • BaseConsumer<T> → abstract class for consumers. Handles message deserialization, ACK/NACK, and error handling. Microservices implement only HandleMessage(), with their own business logic.
Publishing Folder:

Encapsulates publishing logic: JSON serialization, persistent delivery, and correlation ID assignment. This ensures all producers publish consistently.

  • Publisher → encapsulates publishing. Handles JSON serialization, persistence, and correlation IDs. Every service publishes events the same way.
Extensions Folder:

Adds an AddRabbitMq() extension method for DI registration (ConnectionManager, IConnection, IModel). Called from each service’s Program.cs.

  • ServiceCollectionExtensions → integrates RabbitMQ into ASP.NET Core DI. Every service’s Program.cs can just call services.AddRabbitMq(…).
Why this setup matters:
  • Reduces code duplication in each microservice.
  • All services speak the same event “language”.
  • Consumers/publishers are reusable and follow the same patterns.
  • Makes debugging and tracing easier with correlation IDs.
  • Ensures consistent routing and message handling across all services.

Step 2: Implement OrderService

Start the saga by placing orders, and perform compensation if the orchestrator cancels.

OrderService.Application
  • Services/OrderService.cs: Contains the business logic. Validates and saves new orders to the DB, makes payment, and when the order status = Confirmed. After the DB commit, it calls the publisher to raise OrderPlacedEvent.
OrderService.Infrastructure
  • Messaging/Producers/OrderEventPublisher.cs: Publishes OrderPlacedEvent via Publisher from Messaging.Common.
  • Messaging/Consumers/OrderCancelledConsumer.cs: Consumes OrderCancelledEvent. Calls the application layer to update the DB status to Cancelled (Compensation Step).
OrderService.API
  • Program.cs: Loads RabbitMqOptions, ensures Topology, registers OrderEventPublisher and OrderCancelledConsumer.

Why: OrderService is where the saga begins. It triggers the orchestration and also participates in rollback when the orchestrator decides to cancel.

Step 3: Create OrchestratorService (New Microservice)

This is the brain of Saga Orchestration. It coordinates steps, decides success/failure, and publishes the outcome. It has no database; it only uses workflow logic.

OrchestratorService.Application
  • OrchestrationService.cs → separates orchestration logic from handlers (helps readability).
OrchestratorService.Infrastructure
  • Messaging/Consumers/OrderPlacedHandler.cs → consumes OrderPlacedEvent, tells ProductService to reserve stock by publishing a stock request.
  • Messaging/Consumers/StockReservedHandler.cs → consumes StockReservedEvent, publishes OrderConfirmedEvent.
  • Messaging/Consumers/StockReservationFailedHandler.cs → consumes StockReservationFailedEvent, publishes OrderCancelledEvent.
  • Messaging/Producers/OrderEventsPublisher.cs → helper for publishing saga outcome events.
OrchestratorService.API
  • Program.cs → loads RabbitMQ config, ensures topology, registers all three consumers and the publisher.

This keeps orchestration logic centralized. ProductService and NotificationService don’t need to know “what happens next”; they just do their job. The Orchestrator decides the flow.

Step 4: ProductService

Business logic for stock management.

ProductService.Application
  • Services/InventoryService.cs: Contains the logic to check inventory, reduce stock if available, or reject if not.
ProductService.Infrastructure
  • Messaging/Consumers/StockReserveHandler.cs: Consumes stock reserve request (triggered by Orchestrator). Calls ProductServiceApp. Based on the result, publish StockReservedEvent or StockReservationFailedEvent.
  • Messaging/Producers/StockEventPublisher.cs: Uses shared publisher to raise stock events.
ProductService.API
  • Program.cs: Loads RabbitMQOptions, ensures the topology, and registers the consumer and publisher.

ProductService only cares about inventory. It doesn’t know about orders or customers; that logic is centralized in Orchestrator.

Step 5: NotificationService

This service communicates the saga outcome to the customer.

NotificationService.Infrastructure
  • Messaging/Consumers/OrderConfirmedConsumer.cs: Consumes OrderConfirmedEvent, sends success notification. It will delegate the actual business logic to the NotificationService Application layer.
  • Messaging/Consumers/OrderCancelledConsumer.cs: Consumes OrderCancelledEvent, sends cancellation/failure notification. It will delegate the actual business logic to the NotificationService Application layer.
NotificationService.API
  • Program.cs: Loads RabbitMQ options, ensures the topology, and registers the consumers.

Notifications are decoupled. NotificationService doesn’t need to know about stock or orders; it only reacts to final saga outcomes.

In real-world applications such as e-commerce order processing, using Saga not only enhances fault tolerance but also improves user experience by gracefully handling errors. By adopting the Saga Pattern, developers can build scalable, loosely coupled microservices that uphold business integrity without relying on complex distributed transaction protocols.

Leave a Reply

Your email address will not be published. Required fields are marked *