Legend
A strict, composable saga VM for sequential workflows with compensation.
Legend is a dependency-light orchestration runtime that runs sequential workflows with compensation (a.k.a. the Saga pattern). It focuses on compile-time safety, composability, and resumability.
Features
- Compile-time validation: Empty programs and invalid state transitions are compile errors
- Composable blocks: Reusable step sequences that can be nested via the
block!macro - Typestate execution:
New→Paused/Completed/Failedenforced by the type system - Strict compensation: No silent failures - compensation must explicitly succeed or fail
- Async-first: Built with
async_traitfor native async/await support - Serializable state: Pause, persist, and resume workflows across process restarts
- Step timing: Track execution duration for each step with
StepTiming - Storage abstraction:
Storetrait for persisting paused executions withInMemoryStoreincluded - Tracing support: Optional tracing events for each step (enable
tracingfeature)
Quick Start: Travel Booking Saga
A complete flight + hotel booking system with automatic rollback on failure.
use ;
// =============================================================================
// Context & Errors
// =============================================================================
// =============================================================================
// Step 1: Reserve Funds (Bank API)
// =============================================================================
;
// =============================================================================
// Step 2: Book Hotel (Hotel API)
// =============================================================================
;
// =============================================================================
// Step 3: Book Flight (Flight API)
// =============================================================================
;
// =============================================================================
// Step 4: Charge Payment (Bank API)
// =============================================================================
;
// =============================================================================
// Step 5: Send Confirmation
// =============================================================================
;
// =============================================================================
// Define the Travel Booking Saga
// =============================================================================
legend!
// =============================================================================
// Run the Saga
// =============================================================================
let saga = new;
let execution = saga.build;
match execution.start.await
What Happens on Failure?
If the flight booking fails (Step 3), Legend automatically compensates in reverse order:
Book Flight→ Failed (no seats available)- Compensate Hotel → DELETE /api/hotels/reservations/{id}
- Compensate Bank → DELETE /api/bank/holds/{id}
The customer is never charged, and no orphaned reservations remain.
Resume After Pause
// Resume a paused execution (e.g., after manager approval for large bookings)
async
Execution Flow
State Diagram
stateDiagram-v2
[*] --> New: build()
New --> Completed: start() success
New --> Paused: start() pause
New --> Failed: start() error
Paused --> Completed: resume() success
Paused --> Failed: resume() error
Completed --> [*]
Failed --> [*]
Compensation Flowchart
flowchart TD
Start([Start]) --> RF[1. Reserve Funds<br/>Bank API]
RF -->|Continue| BH[2. Book Hotel<br/>Hotel API]
BH -->|Continue| BF[3. Book Flight<br/>Flight API]
BF -->|Continue| CP[4. Charge Payment<br/>Bank API]
CP -->|Continue| SC[5. Send Confirmation]
SC -->|Continue| Success([Completed])
RF -->|Fail| Fail([Failed])
BH -->|Fail| C1[Compensate Bank Hold]
C1 --> Fail
BF -->|Fail| C2[Compensate Hotel]
C2 --> C1
CP -->|Fail| C3[Compensate Flight]
C3 --> C2
SC -->|Fail| C4[Refund Payment]
C4 --> C3
style Success fill:#10b981
style Fail fill:#ef4444
Core Concepts
Step
A unit of work implementing the Step<Ctx, Err> trait:
StepOutcome
Controls execution flow from execute (returned via Result<StepOutcome, E>):
Continue- Proceed to the next stepPause- Suspend execution (can be serialized and resumed later)
Errors are returned via Err(e), which triggers retry (if policy allows) or compensation.
CompensationOutcome
Controls compensation flow (returned via Result<CompensationOutcome, E>):
Completed- Compensation succeededPause- Compensation needs to pause
Errors are returned via Err(e), representing critical failures requiring manual intervention.
RetryPolicy
Configures automatic retries for transient failures:
// No retries (default)
NoRetry
// Retry up to 3 times
retries
// Retry with backoff hint (100ms)
retries_with_backoff
ExecutionResult
The outcome of start() or resume():
match execution.start.await
Composable Blocks
Define reusable step sequences with the block! macro:
use block;
block!
// Blocks implement Step, so they can be nested in programs
legend!
Storage
Legend provides a Store trait for persisting paused executions, enabling workflows to survive process restarts:
use ;
// Create a store (in-memory for development, implement Store for production)
let store = new;
// When execution pauses, save it
if let Paused = execution.start.await
// Later: retrieve and resume
let record = store.get.await?;
let exec: = from_slice?;
let result = exec.resume.await;
// Clean up after completion
store.delete.await?;
Store Trait
Implement the Store trait for custom backends (Redis, PostgreSQL, sled, etc.):
use ;
The InMemoryStore implementation uses parking_lot::RwLock for thread-safe access and is suitable for testing and single-process deployments.
Step Timing
Legend tracks timing for each step execution:
use ;
// After execution completes, access timing data
let timings = execution.state.step_timings;
for timing in timings
StepOutcome values:
Continue- Step proceeded to nextPause- Step paused executionFailed- Step failedCompensated- Step was compensatedCompensationFailed- Compensation failed
Tracing
Enable the tracing feature for step-level tracing events:
[]
= { = "0.1", = ["tracing"] }
Events emitted:
step.start- Step execution beginsstep.end- Step execution ends (with outcome)step.retry- Step is being retriedcompensate.start- Compensation beginscompensate.end- Compensation ends (with outcome)
Type Safety
Legend uses Rust's type system to prevent errors at compile time:
Empty Programs
// This won't compile - HSingle requires at least one step
legend!
Invalid State Transitions
let exec = saga.build;
// Won't compile - New state has no `resume` method
exec.resume.await;
if let Completed = exec.start.await
Testing
Steps are async functions, making them easy to unit test:
async
See src/tests/ for comprehensive examples:
basic.rs- Success path testscompensation.rs- Rollback testsretry.rs- Retry policy testspause.rs- Pause/resume tests
Why Sagas?
Distributed operations often cross multiple resources (databases, payment systems, external APIs). Two-phase commit is heavy and often unavailable. Sagas keep each step locally atomic and rely on compensation to restore invariants on failure.
Use Legend for:
- Payment flows
- Account provisioning
- Document pipelines
- Any multi-step process with meaningful rollback semantics
Design Goals
- Compile-time safety: Invalid programs don't compile
- Strict compensation: No silent failures
- Composability: Reusable blocks that nest via
block!macro - Testability: Each step is an async function over your context type
- Resumability: Serialize execution state, pause, persist, and resume later
Notes on Reliability
- Prefer idempotent steps; retrying becomes safe
- Compensation must be explicit - return
CompletedorCritical - Record enough data in the context for audit trails and troubleshooting
License
MIT