Saga Pattern
The saga pattern coordinates multi-step operations across multiple services with automatic rollback when a step fails. Each step defines a forward action and a compensation action that undoes it.
How it works
Section titled “How it works”- Steps execute in order, each calling an external service
- If a step fails, all previously completed steps are compensated in reverse order
- After compensation, the configured
on_step_failureaction runs (e.g., alert ops team)
graph LR A[Reserve Inventory] --> B[Charge Payment] --> C[Create Shipment] C -->|failure| D[Refund Payment] D --> E[Release Inventory] E --> F[Alert Ops]Configuration
Section titled “Configuration”Set type: saga and define compensate blocks on call steps:
flow: fulfillment-sagaversion: 1type: sagasource: connector: order-service trigger: webhook event: order/confirmedsteps: - name: reserve-inventory call: inventory-api action: reserve params: items: source.line_items compensate: action: release params: reservation_id: result.reservation_id
- name: charge-payment call: payment-api action: charge params: amount: source.total_price compensate: action: refund params: charge_id: result.charge_id
- name: create-shipment call: shipping-api action: create params: address: source.shipping_addresson_step_failure: strategy: compensate-previous then: alert(ops-team)on_error: retry: 2x exponential(60s) then: dead-letterCompensate block
Section titled “Compensate block”Each call step can define a compensate block:
compensate: action: <string> # Required. Compensation action name. params: # Optional. Mapping expressions. reservation_id: result.reservation_idThe params values are parsed as mapping expressions — you can reference source.* fields from the original payload and result.* fields from the call step’s response (stored via store_as).
on_step_failure
Section titled “on_step_failure”on_step_failure: strategy: compensate-previous # Run compensation for all completed steps then: alert(ops-team) # Optional. Action after compensation.Example: Order fulfillment
Section titled “Example: Order fulfillment”flow: order-fulfillment-sagaversion: 1type: sagasource: connector: order-service trigger: webhook event: order/confirmedsteps: - name: reserve-inventory call: inventory-api action: reserve params: items: source.line_items warehouse: source.warehouse_id store_as: reservation compensate: action: release params: reservation_id: result.reservation_id
- name: charge-payment call: payment-api action: charge params: amount: source.total_price currency: source.currency customer_id: source.customer.id store_as: charge compensate: action: refund params: charge_id: result.charge_id amount: source.total_price
- name: create-shipment call: shipping-api action: create params: items: source.line_items address: source.shipping_address reservation_id: result.reservation_id store_as: shipmenton_step_failure: strategy: compensate-previous then: alert(ops-team)on_error: retry: 2x exponential(60s) then: dead-letterIf charge-payment fails:
reserve-inventoryis compensated → callsinventory-apiwithaction: release- Ops team is alerted
If create-shipment fails:
charge-paymentis compensated → callspayment-apiwithaction: refundreserve-inventoryis compensated → callsinventory-apiwithaction: release- Ops team is alerted
When to use saga vs simple retry
Section titled “When to use saga vs simple retry”| Scenario | Approach |
|---|---|
| Single API call might fail transiently | Simple retry (on_error) |
| Multi-step where partial completion is OK | Multi-step flow without compensation |
| Multi-step where partial completion is NOT OK | Saga with compensation |
| Steps have side effects that must be undone | Saga with compensation |
Limitations
Section titled “Limitations”Saga compensation in fyrn is best-effort. If a compensation call itself fails (e.g., the payment API is down when you try to refund), fyrn retries it according to the flow’s on_error policy, then routes the failure to the dead-letter queue. You should monitor dead-letter for failed compensations and handle them manually or via a separate recovery flow.
Other constraints to be aware of:
- No nested sagas. A saga flow cannot invoke another saga flow as a step. If you need multi-level coordination, use separate saga flows connected via flow chaining.
- Compensation params must be deterministic. The
paramsin acompensateblock are evaluated using values captured at execution time (source.*andresult.*). Do not rely on external state that may have changed between the forward action and compensation. - Design compensations to be idempotent. Because compensation may be retried, the target API should handle duplicate calls gracefully. For example, a refund endpoint should accept a
charge_idand return success if the charge was already refunded, rather than refunding twice.
What’s next
Section titled “What’s next”- Flow Chaining — Fire-and-forget child flows
- Fan-out Routing — Route to multiple targets
- DSL Reference — Full saga specification