Recipe: Distributed E-Commerce Saga

In microservice architectures, two-phase commits across distributed databases are slow and fragile. The Saga pattern breaks a multi-service business transaction into discrete local transactions. If any step fails, the saga executes compensating transactions in reverse order to restore consistency.


Saga Flow Diagram

sequenceDiagram
    participant S as Saga Coordinator
    participant P as Payment Service
    participant I as Inventory Service
    participant D as Delivery Service

    Note over S: Forward Execution
    S->>P: 1. Charge Customer (Success)
    S->>I: 2. Reserve Stock (Success)
    S->>D: 3. Book Courier (FAILS)
    
    Note over S: Compensating Rollback
    S->>I: C1. Release Stock
    S->>P: C2. Refund Customer
    Note over S: Saga Completed with Failure

Rust Implementation

Using xstate_patterns::saga::SagaBuilder:

use serde_json::json;
use xstate_patterns::saga::SagaBuilder;
use xstate_statemachine::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build a 3-step saga: Payment -> Inventory -> Delivery
    let saga = SagaBuilder::new("order_saga")
        .step("charge_payment", "refund_payment")
        .step("reserve_inventory", "release_inventory")
        .step("book_delivery", "cancel_delivery")
        .build()?;

    let logic = MachineLogic::dynamic()
        .action("charge_payment", |scope| {
            println!("Forward action: Charging credit card $120.00...");
            scope.context["payment_id"] = json!("ch_849281");
            Ok(())
        })
        .action("refund_payment", |_scope| {
            println!("Compensating action: Refunding credit card $120.00...");
            Ok(())
        })
        .action("reserve_inventory", |scope| {
            println!("Forward action: Reserving item in warehouse A...");
            scope.context["inventory_reserved"] = json!(true);
            Ok(())
        })
        .action("release_inventory", |_scope| {
            println!("Compensating action: Releasing warehouse reservation...");
            Ok(())
        })
        .action("book_delivery", |_scope| {
            println!("Forward action: Booking courier...");
            // Simulate delivery partner outage
            Err(Error::logic("Delivery courier API unavailable"))
        })
        .action("cancel_delivery", |_scope| {
            Ok(())
        });

    let mut interpreter = SyncInterpreter::new(saga, logic);
    interpreter.start()?;

    // Step 1: Charge Payment
    interpreter.send("charge_payment.SUCCESS")?;
    
    // Step 2: Reserve Inventory
    interpreter.send("reserve_inventory.SUCCESS")?;

    // Step 3: Courier fails! Trigger automatic compensation
    interpreter.send("book_delivery.FAILURE")?;

    // The saga automatically executes compensating rollbacks in reverse:
    // 1. release_inventory
    // 2. refund_payment
    // And lands in the terminal compensation complete state:
    assert!(interpreter.matches("order_saga.compensated") || interpreter.matches("order_saga.failed"));
    println!("Saga cleanly compensated all distributed resources.");

    Ok(())
}

Key Advantages

  • Declarative Graph: The entire compensation DAG is validated at compile time by SagaBuilder.
  • Durable Audit Trail: Every step and compensation action is emitted as a structured CloudEvent or persisted to PostgreSQL using xstate-persistence.
  • Zero Orphaned State: If a worker crashes mid-saga, the persisted statechart recovers and continues rolling back precisely where it left off.