#Saga Compensation Patterns โ Deep Dive
Version: 0.43.0 | Updated: 2026-03-28 | Applies to: ranvier-runtime 0.36+ | Category: Cookbook
#Overview
The Saga pattern is Ranvier's approach to managing distributed transactions across
multiple steps. When step N fails, steps 1 through N-1 must be "compensated"
(rolled back) in reverse order. This cookbook covers advanced saga patterns beyond
the basics covered in cookbook_saga_compensation.md.
Prerequisites: cookbook_saga_compensation.md (basic saga patterns)
#1. Pattern Anatomy
Every saga has three components:
Forward Steps: A โ B โ C โ D
Compensation Steps: A'โ B'โ C'
Control: Outcome::Fault triggers compensationKey principles:
- LIFO ordering: Compensations run in reverse (C' โ B' โ A')
- Idempotency: Each compensation must be safe to retry
- Isolation: Each step operates on its own subset of state
#2. Applied Domain: Payment Order Processing
The canonical saga example โ reserve inventory, charge payment, confirm shipping.
use ranvier_runtime::Axon;
use ranvier_core::saga::SagaPolicy;
let order_saga = Axon::typed::<CreateOrderRequest, String>("order-saga")
.with_saga_policy(SagaPolicy::Enabled)
.then(create_order)
.then_compensated(reserve_inventory, release_inventory)
.then_compensated(authorize_payment, refund_payment)
.then_compensated(confirm_shipping, cancel_shipment)
.then(complete_order);#Why Saga here?
Each step has an external side effect (inventory reservation, payment charge, shipping label) that cannot be automatically rolled back by a database transaction. The saga pattern makes compensation explicit and auditable.
#3. Applied Domain: Insurance Claim Processing
Multi-step claim: validate policy โ assess damage โ approve payout โ schedule disbursement.
let claim_saga = Axon::typed::<ClaimRequest, String>("claim-saga")
.with_saga_policy(SagaPolicy::Enabled)
.then(validate_policy)
.then_compensated(assess_damage, void_assessment)
.then_compensated(approve_payout, revoke_approval)
.then_compensated(schedule_disbursement, cancel_disbursement)
.then(close_claim);#Compensation nuances
void_assessment: Marks the assessment as voided, not deleted (audit trail)revoke_approval: Sends notification to the claimant about the reversalcancel_disbursement: Only possible if funds haven't left the account
#4. Applied Domain: Multi-Service Onboarding
User registration: create account โ provision resources โ send welcome email โ activate.
let onboarding_saga = Axon::typed::<SignupRequest, String>("onboarding")
.with_saga_policy(SagaPolicy::Enabled)
.then(create_account)
.then_compensated(provision_resources, deprovision_resources)
.then_compensated(send_welcome_email, send_cancellation_email)
.then(activate_account);#Key insight
create_account has no compensation โ it's the first step. If it fails,
no compensation is needed. The then() (not then_compensated()) distinction
is intentional.
#5. Advanced: Conditional Compensation
Sometimes compensation logic depends on the failure context. Use the Bus to carry failure metadata into compensation transitions.
#[transition]
async fn authorize_payment(
input: serde_json::Value,
bus: &mut Bus,
) -> Outcome<serde_json::Value, String> {
let result = payment_gateway.charge(input["amount"].as_f64().unwrap()).await;
match result {
Ok(tx_id) => {
bus.insert(tx_id); // Store for compensation
Outcome::Next(input)
}
Err(e) => Outcome::Fault(format!("Payment failed: {}", e)),
}
}
#[transition]
async fn refund_payment(
input: serde_json::Value,
bus: &mut Bus,
) -> Outcome<serde_json::Value, String> {
// Only refund if we have a transaction ID
if let Ok(tx_id) = bus.get_cloned::<String>() {
payment_gateway.refund(&tx_id).await.ok();
tracing::warn!(tx_id = %tx_id, "Payment refunded");
}
Outcome::Next(input)
}#6. Testing Saga Compensation
Use ranvier-test to verify compensation ordering:
#[tokio::test]
async fn compensation_runs_in_reverse() {
let saga = Axon::typed::<OrderRequest, String>("test-saga")
.with_saga_policy(SagaPolicy::Enabled)
.then(step_a)
.then_compensated(step_b, comp_b)
.then_compensated(step_c_fails, comp_c)
.then(step_d);
let mut bus = Bus::new();
let result = saga.execute(order, &(), &mut bus).await;
// step_c_fails returns Fault โ comp_c runs, then comp_b
assert!(matches!(result, Outcome::Fault(_)));
}#Quick Reference
| Scenario | Use Saga? | Why |
|---|---|---|
| Multi-service writes | Yes | Each service needs explicit rollback |
| Single DB transaction | No | Use database ACID instead |
| Read-only pipeline | No | Nothing to compensate |
| Idempotent operations | Maybe | Saga adds clarity even if retry is safe |
#See Also
cookbook_saga_compensation.mdโ Basic saga patternssaga-compensationexample โ Pure Axon saga demo (no HTTP)reference-ecommerce-orderexample โ Full HTTP saga with audit