Below is a step-by-step, real-world explanation of Orchestrator Saga Pattern with rollback using .NET Core, exactly the way it is implemented in production systems and expected in senior interviews.
๐ท Problem Statement
You have multiple microservices, each with its own database.
Scenario:Order → Payment → Inventory → Shipping
๐ If any service fails, you must rollback previously completed steps.
❌ You cannot use DB transactions across services
✅ You must use Saga + Compensating Transactions
๐ท What Is Orchestrator Saga Pattern?
A central Saga Orchestrator controls:
The order of service calls
Success/failure handling
Compensation (rollback) logic
The orchestrator does NOT share DBs
It only invokes APIs / publishes commands
๐ท High-Level Flow Diagram
Client
|
v
Saga Orchestrator
|
|---> Order Service (Create Order)
|---> Payment Service (Charge)
|---> Inventory Service (Reserve)
|---> Shipping Service (Create Shipment)
Failure Example
❌ Inventory fails → Orchestrator:
Refund Payment
Cancel Order
๐ท Step-by-Step Implementation in .NET Core
1️⃣ Order Service (Local Transaction)
Order Entity
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; } // Pending, Confirmed, Cancelled
}
Order Controller
[HttpPost("create")]
public async Task<IActionResult> CreateOrder()
{
var order = new Order { Id = Guid.NewGuid(), Status = "Pending" };
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return Ok(order.Id);
}
[HttpPost("cancel/{orderId}")]
public async Task<IActionResult> CancelOrder(Guid orderId)
{
var order = await _db.Orders.FindAsync(orderId);
order.Status = "Cancelled";
await _db.SaveChangesAsync();
return Ok();
}
2️⃣ Payment Service
[HttpPost("charge")]
public async Task<IActionResult> Charge(Guid orderId)
{
// Simulate payment
if (Random.Shared.Next(1, 10) < 3)
return BadRequest("Payment Failed");
_db.Payments.Add(new Payment { OrderId = orderId, Status = "Paid" });
await _db.SaveChangesAsync();
return Ok();
}
[HttpPost("refund")]
public async Task<IActionResult> Refund(Guid orderId)
{
var payment = await _db.Payments.FirstAsync(p => p.OrderId == orderId);
payment.Status = "Refunded";
await _db.SaveChangesAsync();
return Ok();
}
3️⃣ Inventory Service
[HttpPost("reserve")]
public async Task<IActionResult> Reserve(Guid orderId)
{
// Simulate failure
throw new Exception("Inventory not available");
}
4️⃣ Saga Orchestrator (Core Logic)
Orchestrator Controller
[HttpPost("place-order")]
public async Task<IActionResult> PlaceOrder()
{
Guid orderId = Guid.Empty;
try
{
// 1. Create Order
orderId = await _orderClient.CreateOrder();
// 2. Payment
await _paymentClient.Charge(orderId);
// 3. Inventory
await _inventoryClient.Reserve(orderId);
// 4. Shipping
await _shippingClient.Create(orderId);
return Ok("Order Completed");
}
catch (Exception ex)
{
await Compensate(orderId);
return StatusCode(500, ex.Message);
}
}
5️⃣ Compensation Logic (ROLLBACK)
private async Task Compensate(Guid orderId)
{
if (orderId == Guid.Empty) return;
try
{
await _paymentClient.Refund(orderId);
}
catch { /* log */ }
try
{
await _orderClient.CancelOrder(orderId);
}
catch { /* log */ }
}
๐ Each compensation is its own API call
๐ Each service performs local DB rollback
๐ท Important Production Considerations ⭐
1️⃣ Idempotency (VERY IMPORTANT)
Compensation might be called multiple times.
if (payment.Status == "Refunded")
return Ok();
2️⃣ Retry with Polly
Transient failures happen.
Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(2));
3️⃣ Timeout & Circuit Breaker
Avoid hanging orchestrator.
.TimeoutAsync(TimeSpan.FromSeconds(5))
.CircuitBreakerAsync(3, TimeSpan.FromMinutes(1));
4️⃣ Saga State Table (Recommended)
SagaState
{
SagaId,
OrderId,
StepCompleted,
Status
}
So recovery can happen after crash.
5️⃣ Event-Based Orchestrator (Better Scalability)
Instead of HTTP:
Azure Service Bus
Kafka
RabbitMQ
๐ท Why This Works
No distributed transaction
Each service owns its data
Rollback via compensating transactions
Eventual consistency
๐ท Interview-Ready Answer (Short)
“In orchestrator saga pattern, a central service controls the workflow. Each microservice performs a local transaction. If any service fails, the orchestrator triggers compensating transactions—like refunding payment or canceling the order—to logically rollback the system and achieve eventual consistency.”
๐ท When to Use Orchestrator Saga
| Scenario | Use |
|---|---|
| Complex workflows | ✅ |
| Payment flows | ✅ |
| High observability needed | ✅ |
| Simple async flows | ❌ |
If you want next, I can explain:
Event-driven saga using Kafka/Azure Service Bus
Saga persistence & recovery
Real production pitfalls
Comparison with choreography saga
Just tell me ๐
No comments:
Post a Comment