Skip to main content

Sagas & compensations

The Saga pattern trades distributed transactions for compensating actions: each step registers how to undo itself, and on failure the engine rolls completed steps back in reverse order.

Action-level compensation

The primary style attaches an undo to each step:

public function handle(string $orderId): void
{
$this->action(ChargeCard::class, $orderId)
->compensateWith(RefundCard::class, $orderId)
->run();

$this->action(ReserveStock::class, $orderId)
->compensateWith(ReleaseStock::class, $orderId)
->run();

// If this throws, ReleaseStock then RefundCard run automatically.
$this->action(ShipOrder::class, $orderId)->run();
}

Compensation can also be a closure:

$this->action(MakeReservation::class, $id)
->compensateWith(fn () => Reservation::release($id))
->run();

Grouped sagas

saga() expresses a compensation boundary explicitly and exposes group-level policies:

use DiscoveryUkraine\SagaLaraFlow\Enums\CompensationFailurePolicy;

$this->saga()
->onCompensationFailure(CompensationFailurePolicy::Continue)
->compensateInParallel()
->step(ChargeCard::class, $orderId)->compensateWith(RefundCard::class, $orderId)
->step(ReserveStock::class, $orderId)->compensateWith(ReleaseStock::class, $orderId)
->run();
  • compensateInParallel() runs the group's undos concurrently (a single rollback level: together via Bus::batch when queued, sequentially under runSync).
  • compensateStepOnSelfFailure() also compensates a step that itself failed (for non-atomic actions that may leave partial effects) — such compensations must be idempotent.

Failure policies

CompensationFailurePolicy:

  • Stop (default) — halt the rollback on the first failed compensation.
  • Continue — keep rolling back even if one undo fails.

Precedence is action > group > config (sagas.default_compensation_failure_policy). If a compensation itself fails under Stop, a CompensationFailedException surfaces.

Manual compensation

You can trigger a rollback from outside the workflow through the handle:

SagaFlow::loadFlow($runId)->compensate(); // roll back completed steps, then cancel