Saga Pattern
This example demonstrates automatic compensation (rollback) when a workflow fails.
What This Example Shows
- ✅
DefineCompensationfor compensation functions - ✅
WithCompensationto link compensation to activities - ✅ Automatic reverse-order compensation
- ✅ Saga pattern for distributed transactions
- ✅ Rollback on workflow failure
The Problem
In distributed systems, you can’t use traditional database transactions. The Saga pattern solves this with compensation functions that undo completed steps.
Code Overview
Define Compensation Functions
package main
import (
"context"
"fmt"
"github.com/i2y/romancy"
)
// Compensation: Release reserved inventory
var cancelInventoryReservation = romancy.DefineCompensation("cancel_inventory_reservation",
func(ctx context.Context, orderID, itemID string) error {
fmt.Printf("❌ Cancelled reservation for %s\n", itemID)
return nil
},
)
// Compensation: Refund payment
var refundPayment = romancy.DefineCompensation("refund_payment",
func(ctx context.Context, orderID string, amount float64) error {
fmt.Printf("❌ Refunded $%.2f\n", amount)
return nil
},
)Define Activities with Compensation Links
package main
import (
"context"
"errors"
"fmt"
"github.com/i2y/romancy"
)
// Result types
type ReservationResult struct {
ReservationID string `json:"reservation_id"`
ItemID string `json:"item_id"`
}
type PaymentResult struct {
TransactionID string `json:"transaction_id"`
Amount float64 `json:"amount"`
}
type ShipmentResult struct {
ShipmentID string `json:"shipment_id"`
}
// Reserve inventory (linked to cancelInventoryReservation)
var reserveInventory = romancy.DefineActivity("reserve_inventory",
func(ctx context.Context, orderID, itemID string) (ReservationResult, error) {
fmt.Printf("✅ Reserved %s for order %s\n", itemID, orderID)
return ReservationResult{
ReservationID: fmt.Sprintf("RES-%s", itemID),
ItemID: itemID,
}, nil
},
romancy.WithCompensation(cancelInventoryReservation), // Link compensation
)
// Charge payment (linked to refundPayment)
var chargePayment = romancy.DefineActivity("charge_payment",
func(ctx context.Context, orderID string, amount float64) (PaymentResult, error) {
fmt.Printf("✅ Charged $%.2f for order %s\n", amount, orderID)
return PaymentResult{
TransactionID: fmt.Sprintf("TXN-%s", orderID),
Amount: amount,
}, nil
},
romancy.WithCompensation(refundPayment), // Link compensation
)
// Ship order (no compensation - this will fail)
var shipOrder = romancy.DefineActivity("ship_order",
func(ctx context.Context, orderID string) (ShipmentResult, error) {
fmt.Printf("🚚 Attempting to ship order %s\n", orderID)
return ShipmentResult{}, errors.New("shipping service unavailable")
},
)Define Saga Workflow
package main
import (
"github.com/i2y/romancy"
)
// Result type for the saga workflow
type OrderSagaResult struct {
Status string `json:"status"`
}
// orderSaga processes an order with automatic compensation on failure.
// If any step fails, Romancy automatically calls compensation functions
// for all completed steps in reverse order.
var orderSaga = romancy.DefineWorkflow("order_saga",
func(ctx *romancy.WorkflowContext, orderID string) (OrderSagaResult, error) {
// Step 1: Reserve inventory
_, err := reserveInventory.Execute(ctx, orderID, "ITEM-123")
if err != nil {
return OrderSagaResult{}, err
}
// Step 2: Charge payment
_, err = chargePayment.Execute(ctx, orderID, 99.99)
if err != nil {
return OrderSagaResult{}, err
}
// Step 3: Ship order (will fail!)
_, err = shipOrder.Execute(ctx, orderID)
if err != nil {
return OrderSagaResult{}, err
}
return OrderSagaResult{Status: "completed"}, nil
},
)Expected Output
✅ Reserved ITEM-123 for order ORD-001
✅ Charged $99.99 for order ORD-001
🚚 Attempting to ship order ORD-001
💥 Error: shipping service unavailable
Automatic compensation (reverse order):
❌ Refunded $99.99
❌ Cancelled reservation for ITEM-123
Workflow failed with compensation completed.How It Works
- Step 1 completes: Inventory reserved ✅
- Step 2 completes: Payment charged ✅
- Step 3 fails: Shipping fails ❌
- Automatic compensation (reverse order):
- First: Refund payment (Step 2 compensation)
- Then: Cancel reservation (Step 1 compensation)
Key Rules
1. Reverse Order Execution
Compensation functions run in reverse order of activity execution:
Activities: reserve → charge → ship (fails)
Compensations: cancel ← refund2. Only Completed Activities
Only successfully completed activities are compensated:
reserveInventory.Execute(ctx, ...) // ✅ Completed → Will be compensated
chargePayment.Execute(ctx, ...) // ✅ Completed → Will be compensated
shipOrder.Execute(ctx, ...) // ❌ Failed → No compensation needed3. Automatic Trigger
No manual compensation trigger required - Romancy handles it automatically on workflow failure.
Real-World Use Cases
- E-commerce: Reserve inventory → Charge payment → Ship order
- Hotel Booking: Reserve room → Charge deposit → Send confirmation
- Travel: Book flight → Book hotel → Rent car
- Financial: Transfer funds → Update ledger → Send receipt
Running the Example
Create a file named saga_example.go with the complete code (see below), then run:
# Initialize Go module
go mod init saga-example
go get github.com/i2y/romancy
# Run your workflow
go run saga_example.goComplete Code
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/i2y/romancy"
)
// Result types
type ReservationResult struct {
ReservationID string `json:"reservation_id"`
ItemID string `json:"item_id"`
}
type PaymentResult struct {
TransactionID string `json:"transaction_id"`
Amount float64 `json:"amount"`
}
type ShipmentResult struct {
ShipmentID string `json:"shipment_id"`
}
type OrderSagaResult struct {
Status string `json:"status"`
}
// Compensation functions
var cancelInventoryReservation = romancy.DefineCompensation("cancel_inventory_reservation",
func(ctx context.Context, orderID, itemID string) error {
fmt.Printf("❌ Cancelled reservation for %s\n", itemID)
return nil
},
)
var refundPayment = romancy.DefineCompensation("refund_payment",
func(ctx context.Context, orderID string, amount float64) error {
fmt.Printf("❌ Refunded $%.2f\n", amount)
return nil
},
)
// Activities with compensation links
var reserveInventory = romancy.DefineActivity("reserve_inventory",
func(ctx context.Context, orderID, itemID string) (ReservationResult, error) {
fmt.Printf("✅ Reserved %s for order %s\n", itemID, orderID)
return ReservationResult{
ReservationID: fmt.Sprintf("RES-%s", itemID),
ItemID: itemID,
}, nil
},
romancy.WithCompensation(cancelInventoryReservation),
)
var chargePayment = romancy.DefineActivity("charge_payment",
func(ctx context.Context, orderID string, amount float64) (PaymentResult, error) {
fmt.Printf("✅ Charged $%.2f for order %s\n", amount, orderID)
return PaymentResult{
TransactionID: fmt.Sprintf("TXN-%s", orderID),
Amount: amount,
}, nil
},
romancy.WithCompensation(refundPayment),
)
var shipOrder = romancy.DefineActivity("ship_order",
func(ctx context.Context, orderID string) (ShipmentResult, error) {
fmt.Printf("🚚 Attempting to ship order %s\n", orderID)
return ShipmentResult{}, errors.New("shipping service unavailable")
},
)
// Saga workflow
var orderSaga = romancy.DefineWorkflow("order_saga",
func(ctx *romancy.WorkflowContext, orderID string) (OrderSagaResult, error) {
// Step 1: Reserve inventory
_, err := reserveInventory.Execute(ctx, orderID, "ITEM-123")
if err != nil {
return OrderSagaResult{}, err
}
// Step 2: Charge payment
_, err = chargePayment.Execute(ctx, orderID, 99.99)
if err != nil {
return OrderSagaResult{}, err
}
// Step 3: Ship order (will fail!)
_, err = shipOrder.Execute(ctx, orderID)
if err != nil {
return OrderSagaResult{}, err
}
return OrderSagaResult{Status: "completed"}, nil
},
)
func main() {
fmt.Println("============================================================")
fmt.Println("Romancy Framework - Saga Pattern Example")
fmt.Println("============================================================")
fmt.Println()
// Create Romancy app
app := romancy.NewApp(
romancy.WithDatabase("saga_demo.db"),
romancy.WithWorkerID("worker-1"),
)
ctx := context.Background()
// Start the app
if err := app.Start(ctx); err != nil {
log.Fatal(err)
}
defer app.Shutdown(ctx)
fmt.Println(">>> Starting saga workflow...")
fmt.Println()
// Start the saga workflow
instanceID, err := romancy.StartWorkflow(ctx, app, orderSaga, "ORD-001")
if err != nil {
fmt.Printf("\n>>> Workflow failed (compensation was triggered): %v\n", err)
} else {
fmt.Printf("\n>>> Workflow completed with instance ID: %s\n", instanceID)
}
}What You Learned
- ✅
DefineCompensation: Define compensation functions - ✅
WithCompensation: Link compensation to activities - ✅ Automatic Execution: Romancy handles compensation automatically
- ✅ Reverse Order: Compensations run in reverse order
- ✅ Saga Pattern: Distributed transaction management without 2PC
Next Steps
- Event Waiting: Wait for external events
- Core Concepts: Deep dive into Saga pattern