Saga & Compensation
Long-running business workflows often commit real-world side effects — money is charged, stock is reserved, shipments are created. When a later step fails, those committed effects must be undone. BLOGE makes this declarative: attach a compensate operator to any node and let an optional graph-level saga { ... } block control how compensation runs.
The idea
Each node can declare a compensating action. If the graph fails after that node has already succeeded, BLOGE runs the compensation to roll the effect back. You never hand-write an orchestrator that remembers what to undo — the graph remembers for you.
graph checkout {
saga {
order = backward // backward | forward | custom
on_failure = compensate // compensate | skip | fail_fast
max_compensation_retries = 2
}
node reserveStock : ReserveStockOperator {
compensate releaseStock : ReleaseStockOperator
}
node chargeCard : ChargeCardOperator {
depends_on = [reserveStock]
compensate refundCard : RefundCardOperator {
retry = { attempts: 3, backoff: 200ms }
}
}
node createShipment : CreateShipmentOperator {
depends_on = [chargeCard]
}
}If createShipment fails, BLOGE compensates the already-committed nodes — chargeCard then reserveStock — in backward order.
The saga block
The optional graph-level saga { ... } block tunes how compensation behaves across the whole graph.
| Setting | Values | Meaning |
|---|---|---|
order | backward, forward, custom | Order in which committed nodes are compensated |
on_failure | compensate, skip, fail_fast | What to do when a compensation step itself fails |
max_compensation_retries | integer | Default retry budget applied to compensation steps |
backwardunwinds in reverse completion order — the most common saga pattern.forwardcompensates in original completion order.customlets you take explicit control of ordering.
Per-node compensation
Any node may declare its own compensate operator. The compensation block can also carry its own retry = { ... } policy that overrides the saga-level default:
node chargeCard : ChargeCardOperator {
compensate refundCard : RefundCardOperator {
retry = { attempts: 3, backoff: 200ms, strategy: exponential }
}
}Inspecting outcomes
Compensation results surface per node on the graph result so you can audit exactly what was undone:
GraphResult result = engine.execute(graph, context);
if (!result.isSuccess()) {
result.compensationResults().forEach((nodeId, outcome) ->
log.info("compensated {} -> {}", nodeId, outcome));
}When to reach for a saga
- A workflow performs irreversible-looking side effects that actually have an inverse (charge ↔ refund, reserve ↔ release, book ↔ cancel).
- You want rollback semantics visible in the graph definition instead of buried in service code.
- You need per-node retry budgets for compensation so a flaky refund endpoint still settles.
For durability across process restarts, combine sagas with Durable Flows and Crash Recovery.
Next steps
- Pair compensation with Resilience Policies on the forward path.
- Persist execution state with Durable Flows.
- Audit every step with the Event Journal & Ops Console.