Saga Dry Run
Scenario: Place Order → Charge Payment → Deduct Stock (Stock fails)¶
Choreography Dry Run¶
Each service just reacts to events. No central brain.
Step 1 — Order placed:
OrderService
→ saves order to its DB
→ publishes "OrderCreated" (orderId: 123)
Step 2 — Payment reacts:
PaymentService
→ hears "OrderCreated"
→ charges card
→ publishes "PaymentCharged" (orderId: 123)
Step 3 — Inventory reacts, fails:
InventoryService
→ hears "PaymentCharged"
→ tries to deduct stock
→ not enough stock ❌
→ publishes "StockDeductionFailed" (orderId: 123)
Step 4 — Payment compensates:
PaymentService
→ hears "StockDeductionFailed"
→ refunds the card
→ publishes "PaymentRefunded" (orderId: 123)
Step 5 — Order compensates:
OrderService
→ hears "PaymentRefunded"
→ cancels the order
→ done ✅
Nobody tracked state. OrderId in every message was enough to know which order to act on. Each service just knew its own job and its own undo.
Orchestration Dry Run¶
Orchestrator owns the flow. Services just do what they're told.
Step 1 — Order placed:
OrderService
→ saves order to its DB
→ publishes "OrderCreated" (orderId: 123)
Step 2 — Orchestrator kicks off the saga:
Orchestrator
→ hears "OrderCreated"
→ creates saga row in DB:
{ correlationId: abc, orderId: 123, state: "PaymentPending", paymentCharged: false, stockDeducted: false }
→ issues command "ChargePayment" (orderId: 123)
Step 3 — Payment does its job:
PaymentService
→ hears "ChargePayment" command
→ charges card
→ publishes "PaymentCharged" (orderId: 123)
→ has no idea it's part of a saga
Step 4 — Orchestrator advances:
Orchestrator
→ hears "PaymentCharged"
→ finds saga row by correlationId
→ updates row:
{ state: "InventoryPending", paymentCharged: true }
→ issues command "DeductStock" (orderId: 123)
Step 5 — Inventory fails:
InventoryService
→ hears "DeductStock" command
→ not enough stock ❌
→ publishes "StockDeductionFailed" (orderId: 123)
→ has no idea it's part of a saga
Step 6 — Orchestrator sees the failure:
Orchestrator
→ hears "StockDeductionFailed"
→ finds saga row
→ sees: paymentCharged: true, stockDeducted: false
→ knows: refund payment, no need to restore stock
→ updates row:
{ state: "Compensating" }
→ issues command "RefundPayment" (orderId: 123)
Step 7 — Payment compensates:
PaymentService
→ hears "RefundPayment" command
→ refunds card
→ publishes "PaymentRefunded" (orderId: 123)
Step 8 — Orchestrator closes the saga:
Orchestrator
→ hears "PaymentRefunded"
→ issues command "CancelOrder" (orderId: 123)
→ updates row: { state: "Failed" }
OrderService
→ hears "CancelOrder"
→ cancels the order
→ done ✅
Side by side takeaway:¶
| Choreography | Orchestration | |
|---|---|---|
| Who decides next step | Each service itself | Orchestrator |
| State stored | Nowhere | Saga DB row |
| Services aware of saga | Yes, they listen for failure events | No, they just follow commands |
| Easy to follow flow | ❌ spread everywhere | ✅ all in orchestrator |
| Good for | Simple short flows | Complex, long, auditable flows |