Skip to content

Sequence: Saga Compensation (Payment Failure)

When payment capture fails after stock has been reserved, the Orders service must compensate by releasing the reservation. Without compensation, stock would remain locked indefinitely.

sequenceDiagram
    autonumber
    actor Customer
    participant GW as API Gateway (Kong)
    participant OS as Orders Service
    participant IS as Inventory Service
    participant PG as Payment Gateway
    participant ES as EventStoreDB
    participant KB as Kafka

    Customer->>GW: POST /v1/orders {items, paymentToken}
    GW->>OS: POST /orders

    OS->>ES: Append OrderPlaced event
    OS->>KB: Publish OrderPlaced

    OS->>IS: ReserveStock(orderId, items)
    IS-->>OS: StockReserved (reservationId)

    OS->>ES: Append OrderAwaitingPayment event

    OS->>PG: CapturePayment(paymentToken, total)
    PG-->>OS: PaymentFailed (reason: insufficient funds)

    Note over OS: Compensation begins
    OS->>IS: ReleaseReservation(reservationId)
    IS->>IS: Release row lock; increment stock levels
    IS-->>OS: ReservationReleased
    IS->>KB: Publish StockReleased → chakra.inventory.released

    OS->>ES: Append OrderCancelled event (reason: payment_failed)
    OS->>KB: Publish OrderCancelled → chakra.orders.cancelled

    OS-->>GW: 402 Payment Required
    GW-->>Customer: 402 Payment Required

Compensation Guarantees

Guarantee Mechanism
Stock is always released on payment failure Saga orchestrator calls ReleaseReservation before appending OrderCancelled
No partial state persists EventStoreDB append is atomic; compensation is complete before the saga exits
Downstream consumers see consistent events StockReleased and OrderCancelled are both published before the response is returned
Idempotent compensation ReleaseReservation is idempotent — safe to retry if the call times out