SAGA Pattern¶
The SAGA pattern provides compensating transactions for workflows where true rollback is impossible. When you call an external API, send an email, or charge a credit card, you can't "un-send" the email or "un-call" the API — but you can execute a compensating action (refund the charge, send a cancellation, etc.).
When to Use SAGAs¶
Use atomic groups with on_failure: rollback when:
- All operations are local (database writes, in-memory state).
- You can restore to a previous snapshot.
Use SAGAs when:
- Operations involve external APIs (payment gateways, shipping providers).
- Operations are irreversible but have known compensations (charge → refund).
- You need best-effort cleanup rather than guaranteed rollback.
How It Works¶
The SAGA Coordinator executes steps in order. If a step fails:
- Stop executing forward.
- Walk backward through completed steps.
- Execute each step's
compensate_handler(if defined). - Continue compensating even if individual compensations fail.
- Raise
SagaCompensationErrorwith details of any compensation failures.
Forward execution: Step 1 → Step 2 → Step 3 (FAILS)
Compensation (reverse): Step 2.compensate → Step 1.compensate
SagaCoordinator¶
from honey_badgeria.back.atomicity.saga import SagaCoordinator, SagaStep
saga = SagaCoordinator(
name="process_payment",
backend=transaction_backend,
logger=ai_logger,
)
Adding Steps¶
saga.add_step(SagaStep(
name="validate_card",
vertex_name="validate_card",
compensate_handler=None, # Nothing to undo for validation
))
saga.add_step(SagaStep(
name="charge_card",
vertex_name="charge_card",
compensate_handler=lambda inputs, result: refund_api(result["txn_id"]),
))
saga.add_step(SagaStep(
name="send_receipt",
vertex_name="send_receipt",
compensate_handler=lambda inputs, result: send_cancellation(result["email"]),
))
SagaStep Fields¶
| Field | Type | Description |
|---|---|---|
name |
str |
Step identifier |
vertex_name |
str |
Vertex to execute |
compensate_handler |
Callable \| None |
Function to undo this step (receives inputs and result) |
Executing¶
Synchronous:
Asynchronous:
The step_executor_fn is a callable that receives a step and returns the step's result:
Compensation Behavior¶
Success Path¶
All steps execute forward. No compensation needed.
Failure Path¶
If send_receipt fails:
validate_card ✓ → charge_card ✓ → send_receipt ✗
│
Compensate: │
charge_card.compensate ← (refund)
validate_card.compensate ← (None, skipped)
send_receiptfails — stop forward execution.- Walk backward:
charge_cardhas a compensate handler → execute it (refund). - Walk backward:
validate_cardhas no compensate handler → skip. - Raise
SagaStepErrorwith the original exception.
Compensation Failures¶
Compensation is best-effort. If a compensate handler also fails:
HBIA logs the failure but continues compensating remaining steps. After all compensations are attempted, it raises SagaCompensationError containing a list of all compensation failures.
Example: Order Processing¶
from honey_badgeria.back.atomicity.saga import SagaCoordinator, SagaStep
saga = SagaCoordinator("process_order", backend, logger)
# Step 1: Reserve inventory
saga.add_step(SagaStep(
name="reserve_inventory",
vertex_name="reserve_inventory",
compensate_handler=lambda inputs, result: release_inventory(result["items"]),
))
# Step 2: Charge customer
saga.add_step(SagaStep(
name="charge_customer",
vertex_name="charge_customer",
compensate_handler=lambda inputs, result: refund_payment(result["payment_id"]),
))
# Step 3: Schedule shipping
saga.add_step(SagaStep(
name="schedule_shipping",
vertex_name="schedule_shipping",
compensate_handler=lambda inputs, result: cancel_shipment(result["tracking_id"]),
))
# Step 4: Send confirmation email
saga.add_step(SagaStep(
name="send_confirmation",
vertex_name="send_confirmation",
compensate_handler=lambda inputs, result: send_cancellation_email(result["email"]),
))
# Execute
try:
results = saga.execute(step_executor)
print("Order processed successfully")
except SagaStepError as e:
print(f"Order failed at step '{e.step_name}': {e.original}")
# Compensations have already been attempted
except SagaCompensationError as e:
print(f"Order failed AND some compensations failed: {e.failures}")
# Manual intervention may be needed
SAGA Events¶
The AILogger records all SAGA events for observability:
| Event | When |
|---|---|
SAGA_STARTED |
SAGA execution begins |
SAGA_STEP_STARTED |
Each step begins |
SAGA_STEP_COMPLETED |
Each step succeeds |
SAGA_STEP_FAILED |
A step fails |
SAGA_COMPLETED |
SAGA execution finishes (success or failure) |
from honey_badgeria.logging import AILogger, EventType
logger = AILogger("hbia")
# Events are automatically recorded during SAGA execution
events = logger.get_events()
Atomicity vs. SAGA: Decision Guide¶
| Question | Use Atomic Group | Use SAGA |
|---|---|---|
| All operations are local? | ✓ | |
| Operations involve external APIs? | ✓ | |
| True rollback is possible? | ✓ | |
| Need compensating reactions? | ✓ | |
| Need guaranteed consistency? | ✓ | |
| Need best-effort cleanup? | ✓ |