Skip to content

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.

bloge
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.

SettingValuesMeaning
orderbackward, forward, customOrder in which committed nodes are compensated
on_failurecompensate, skip, fail_fastWhat to do when a compensation step itself fails
max_compensation_retriesintegerDefault retry budget applied to compensation steps
  • backward unwinds in reverse completion order — the most common saga pattern.
  • forward compensates in original completion order.
  • custom lets 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:

bloge
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:

java
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