Skip to content

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.

  1. Steps execute in order, each calling an external service
  2. If a step fails, all previously completed steps are compensated in reverse order
  3. After compensation, the configured on_step_failure action 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]

Set type: saga and define compensate blocks on call steps:

flow: fulfillment-saga
version: 1
type: saga
source:
connector: order-service
trigger: webhook
event: order/confirmed
steps:
- 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_address
on_step_failure:
strategy: compensate-previous
then: alert(ops-team)
on_error:
retry: 2x exponential(60s)
then: dead-letter

Each call step can define a compensate block:

compensate:
action: <string> # Required. Compensation action name.
params: # Optional. Mapping expressions.
reservation_id: result.reservation_id

The 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:
strategy: compensate-previous # Run compensation for all completed steps
then: alert(ops-team) # Optional. Action after compensation.

flow: order-fulfillment-saga
version: 1
type: saga
source:
connector: order-service
trigger: webhook
event: order/confirmed
steps:
- 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: shipment
on_step_failure:
strategy: compensate-previous
then: alert(ops-team)
on_error:
retry: 2x exponential(60s)
then: dead-letter

If charge-payment fails:

  1. reserve-inventory is compensated → calls inventory-api with action: release
  2. Ops team is alerted

If create-shipment fails:

  1. charge-payment is compensated → calls payment-api with action: refund
  2. reserve-inventory is compensated → calls inventory-api with action: release
  3. Ops team is alerted

ScenarioApproach
Single API call might fail transientlySimple retry (on_error)
Multi-step where partial completion is OKMulti-step flow without compensation
Multi-step where partial completion is NOT OKSaga with compensation
Steps have side effects that must be undoneSaga with compensation

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 params in a compensate block are evaluated using values captured at execution time (source.* and result.*). 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_id and return success if the charge was already refunded, rather than refunding twice.