Skip to content

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