Saga — e-commerce checkout
A multi-step checkout with compensating transactions: when a later step fails, completed steps roll back in reverse order.
A multi-step checkout with compensating transactions: when a later step fails, completed steps roll back in reverse order.
A checkout touches several systems — inventory, payments, shipping — with no distributed transaction across them. The saga pattern models each forward step with a compensator; if a later step fails, FlexiQ runs the compensators for the already-completed steps in reverse-dependency order.
Declare each forward task and its rollback. A compensator receives the forward step's result as its payload, so it knows exactly what to undo.
public final class Tasks {
public record Reservation(String reservationId) {}
public record Charge(String chargeId) {}
public record Shipment(String shipmentId) {}
public static final Task<Order> RESERVE = Task.of("reserve_inventory", Order.class);
public static final Task<Reservation> RELEASE = Task.of("release_inventory", Reservation.class);
public static final Task<Order> CHARGE = Task.of("charge_payment", Order.class);
public static final Task<Charge> REFUND = Task.of("refund_payment", Charge.class);
public static final Task<Order> SHIP = Task.of("create_shipment", Order.class);
public static final Task<Shipment> CANCEL_SHIPMENT = Task.of("cancel_shipment", Shipment.class);
private Tasks() {}
}Each step names its compensate task. The steps run in dependency order; the
compensators are wired automatically.
import org.byteveda.flexiq.FlexiQ;
import org.byteveda.flexiq.workflows.Step;
import org.byteveda.flexiq.workflows.Workflow;
import org.byteveda.flexiq.workflows.WorkflowRun;
public final class Checkout {
public static WorkflowRun submit(FlexiQ flexiq, Order order) {
Workflow checkout = Workflow.named("checkout")
.step(Step.of("reserve", Tasks.RESERVE, order)
.compensate(Tasks.RELEASE)
.build())
.step(Step.of("charge", Tasks.CHARGE, order)
.after("reserve")
.compensate(Tasks.REFUND)
.build())
.step(Step.of("ship", Tasks.SHIP, order)
.after("charge")
.compensate(Tasks.CANCEL_SHIPMENT)
.build());
return flexiq.submitWorkflow(checkout);
}
private Checkout() {}
}The worker registers the forward and compensation handlers, and tracks
workflows so the saga orchestrator can drive the rollback. Start it before
submitting — an unclaimed job never progresses, so run.await(...) would
otherwise time out.
import java.time.Duration;
import org.byteveda.flexiq.FlexiQ;
import org.byteveda.flexiq.worker.Worker;
import org.byteveda.flexiq.workflows.WorkflowRun;
import org.byteveda.flexiq.workflows.WorkflowStatus;
try (FlexiQ flexiq = FlexiQ.builder().sqlite("checkout.db").open();
Worker worker = flexiq.worker()
.handle(Tasks.RESERVE, order -> inventory.reserve(order))
.handle(Tasks.RELEASE, reservation -> inventory.release(reservation))
.handle(Tasks.CHARGE, order -> payments.charge(order))
.handle(Tasks.REFUND, charge -> payments.refund(charge))
.handle(Tasks.SHIP, order -> shipping.create(order))
.handle(Tasks.CANCEL_SHIPMENT, shipment -> shipping.cancel(shipment))
.trackWorkflows()
.start()) {
WorkflowRun run = Checkout.submit(flexiq, order);
WorkflowStatus done = run.await(Duration.ofMinutes(1));
switch (done.state) {
case COMPLETED -> System.out.println("order placed");
case COMPENSATED -> System.out.println("rolled back cleanly — customer charged nothing");
case COMPENSATION_FAILED -> System.err.println("manual intervention needed");
default -> System.out.println(done.state.wire());
}
}If create_shipment throws, reserve and charge have already completed.
FlexiQ runs their compensators in reverse order:
ship ✗ failed
charge → refund_payment(Charge{chargeId}) (compensate)
reserve → release_inventory(Reservation{reservationId}) (compensate)
The run lands in COMPENSATED if every compensator succeeds, or
COMPENSATION_FAILED if one of them throws. Independent rollbacks in the same
wave run in parallel; each compensation job carries an idempotency key, so a
tracker restart never double-dispatches one.
Compensators should be idempotent. A refund or release may be retried, and a partially-applied rollback that re-runs must not double-refund or release twice.
WorkflowState | Meaning |
|---|---|
COMPLETED | every forward step succeeded |
COMPENSATING | a step failed; compensators are running |
COMPENSATED | rollback finished cleanly |
COMPENSATION_FAILED | a compensator threw — needs manual repair |
| Pattern | Where |
|---|---|
| Forward + rollback pair | Step.of(...).compensate(task) |
| Reverse-order rollback | automatic on step failure |
| Result-aware undo | compensator receives the forward result |
| Outcome branching | switch (done.state) |