The Saga pattern is a design pattern that addresses the complexities involved in managing distributed transactions and ensuring data consistency across microservices. It breaks down a long-running transaction into a series of smaller, manageable transactions. Each transaction is coordinated by a saga orchestrator or via a choreography approach. If any transaction fails, the pattern ensures compensating actions are executed to maintain data integrity.
Why Use the Saga Pattern?
- Data Consistency: Ensures eventual consistency across microservices.
- Resilience: Improves fault tolerance by handling failures gracefully.
- Scalability: Better suited for distributed systems, avoiding the need for distributed transactions.
Approaches to Implementing the Saga Pattern
- Orchestration: A central coordinator (orchestrator) manages the entire saga's flow.
- Choreography: Each service listens for events and decides when to act and when to trigger the next step.
Example. Order Processing System.
We will implement a simple order processing system using the orchestration approach. The system will involve the following services:
- Order Service: Handles order creation.
- Inventory Service: Manages inventory stock.
- Payment Service: Processes payments.
- Saga Orchestrator: Coordinates the saga's flow.
Step-by-Step Implementation
Step 1. Setting Up the Services
First, create a new ASP.NET Core project for each service.
dotnet new webapi -n OrderService
dotnet new webapi -n InventoryService
dotnet new webapi -n PaymentService
dotnet new webapi -n SagaOrchestrator
Step 2. Defining the Models
Define the models shared across the services.
OrderService/Models/Order.cs
public class Order
{
public int Id { get; set; }
public string ProductId { get; set; }
public int Quantity { get; set; }
public string Status { get; set; }
}
InventoryService/Models/Inventory.cs
public class Inventory
{
public string ProductId { get; set; }
public int Stock { get; set; }
}
PaymentService/Models/Payment.cs
public class Payment
{
public int OrderId { get; set; }
public string Status { get; set; }
public decimal Amount { get; set; }
}
Step 3. Creating the Saga Orchestrator
The orchestrator manages the workflow of the saga.
SagaOrchestrator/Controllers/SagaController.cs
[ApiController]
[Route("[controller]")]
public class SagaController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
public SagaController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpPost]
public async Task<IActionResult> ProcessOrder([FromBody] Order order)
{
order.Status = "Pending";
// Step 1: Create Order
var orderResponse = await CreateOrder(order);
if (!orderResponse.IsSuccessStatusCode)
{
return BadRequest("Order creation failed.");
}
// Step 2: Reserve Inventory
var inventoryResponse = await ReserveInventory(order);
if (!inventoryResponse.IsSuccessStatusCode)
{
await CancelOrder(order.Id);
return BadRequest("Inventory reservation failed.");
}
// Step 3: Process Payment
var paymentResponse = await ProcessPayment(order);
if (!paymentResponse.IsSuccessStatusCode)
{
await ReleaseInventory(order.ProductId, order.Quantity);
await CancelOrder(order.Id);
return BadRequest("Payment processing failed.");
}
order.Status = "Completed";
return Ok(order);
}
private async Task<HttpResponseMessage> CreateOrder(Order order)
{
var client = _httpClientFactory.CreateClient();
return await client.PostAsJsonAsync("http://localhost:5001/api/orders", order);
}
private async Task<HttpResponseMessage> ReserveInventory(Order order)
{
var client = _httpClientFactory.CreateClient();
return await client.PostAsJsonAsync("http://localhost:5002/api/inventory/reserve", new { order.ProductId, order.Quantity });
}
private async Task<HttpResponseMessage> ProcessPayment(Order order)
{
var client = _httpClientFactory.CreateClient();
return await client.PostAsJsonAsync("http://localhost:5003/api/payments", new { order.Id, Amount = order.Quantity * 10 });
}
private async Task<HttpResponseMessage> CancelOrder(int orderId)
{
var client = _httpClientFactory.CreateClient();
return await client.DeleteAsync($"http://localhost:5001/api/orders/{orderId}");
}
private async Task<HttpResponseMessage> ReleaseInventory(string productId, int quantity)
{
var client = _httpClientFactory.CreateClient();
return await client.PostAsJsonAsync("http://localhost:5002/api/inventory/release", new { productId, quantity });
}
}
Step 4. Implementing the Services
OrderService/Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private static readonly List<Order> Orders = new();
[HttpPost]
public IActionResult CreateOrder([FromBody] Order order)
{
order.Id = Orders.Count + 1;
order.Status = "Created";
Orders.Add(order);
return CreatedAtAction(nameof(GetOrderById), new { id = order.Id }, order);
}
[HttpDelete("{id}")]
public IActionResult CancelOrder(int id)
{
var order = Orders.FirstOrDefault(o => o.Id == id);
if (order == null)
{
return NotFound();
}
Orders.Remove(order);
return NoContent();
}
[HttpGet("{id}")]
public IActionResult GetOrderById(int id)
{
var order = Orders.FirstOrDefault(o => o.Id == id);
if (order == null)
{
return NotFound();
}
return Ok(order);
}
}
InventoryService/Controllers/InventoryController.cs
[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private static readonly List<Inventory> Inventories = new()
{
new Inventory { ProductId = "Product1", Stock = 100 },
new Inventory { ProductId = "Product2", Stock = 200 }
};
[HttpPost("reserve")]
public IActionResult ReserveInventory([FromBody] dynamic request)
{
string productId = request.productId;
int quantity = request.quantity;
var inventory = Inventories.FirstOrDefault(i => i.ProductId == productId);
if (inventory == null || inventory.Stock < quantity)
{
return BadRequest("Insufficient stock.");
}
inventory.Stock -= quantity;
return Ok();
}
[HttpPost("release")]
public IActionResult ReleaseInventory([FromBody] dynamic request)
{
string productId = request.productId;
int quantity = request.quantity;
var inventory = Inventories.FirstOrDefault(i => i.ProductId == productId);
if (inventory == null)
{
return NotFound();
}
inventory.Stock += quantity;
return Ok();
}
}
PaymentService/Controllers/PaymentsController.cs
[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
private static readonly List<Payment> Payments = new();
[HttpPost]
public IActionResult ProcessPayment([FromBody] dynamic request)
{
int orderId = request.orderId;
decimal amount = request.amount;
var payment = new Payment
{
OrderId = orderId,
Amount = amount,
Status = "Processed"
};
Payments.Add(payment);
return Ok(payment);
}
}
Step 5. Running the Application
- Start the Services: Run each service (OrderService, InventoryService, PaymentService, SagaOrchestrator) on different ports.
- Test the Saga: Use a tool like Postman to send a POST request to the SagaOrchestrator's /saga endpoint with an order payload.
{
"productId": "Product1",
"quantity": 10
}
Conclusion
The Saga pattern is crucial for managing distributed transactions and ensuring data consistency across microservices. By breaking down a transaction into smaller steps and coordinating them through an orchestrator, we can handle failures gracefully and maintain data integrity. The provided example demonstrates how to implement the Saga pattern using ASP.NET Core, handling order processing across multiple services.