Skip to main content
Version: 0.1.0

Getting started

This tutorial builds a realistic multi-step saga from empty, in embedded mode (no infrastructure).

1. Installโ€‹

go get github.com/Bugs5382/go-saga-orchestration

2. A minimal sagaโ€‹

package main

import (
"context"
"fmt"

"github.com/Bugs5382/go-saga-orchestration/saga"
"github.com/Bugs5382/go-saga-orchestration/domain"
"github.com/Bugs5382/go-saga-orchestration/engine/verbs"
)

func main() {
sc := saga.InMemory() // in-memory store + in-process advance

sc.RegisterVerb("charge_card", "common",
verbs.HandlerFunc(func(ctx context.Context, run domain.SagaRun, step domain.Step) (map[string]any, error) {
return map[string]any{"ok": true}, nil
}))

sc.Register(domain.WorkflowDefinition{
ID: "checkout", Version: 1, Start: "charge", Published: true,
Steps: []domain.Step{
{ID: "charge", Type: "charge_card", Next: "done"},
{ID: "done", Type: domain.StepTypeEnd},
},
})

runID, _ := sc.Start(context.Background(), "checkout", map[string]any{"total": 4200})
run, _ := sc.Get(context.Background(), runID)
fmt.Println(run.State) // succeeded
}

3. Recovering from errors with try_catchโ€‹

When a step returns an error, the run normally transitions to failed. To recover instead, wrap the risky steps in a try_catch frame: if any step inside the protected region errors, the saga jumps to your catch step rather than failing, and the error is written to Variables._error.

sc := saga.InMemory()

// A step that fails.
sc.RegisterVerb("charge_card", "common",
verbs.HandlerFunc(func(_ context.Context, _ domain.SagaRun, _ domain.Step) (map[string]any, error) {
return nil, fmt.Errorf("gateway declined")
}))

// The catch handler โ€” it can read the error context off Variables._error.
sc.RegisterVerb("notify_ops", "common",
verbs.HandlerFunc(func(_ context.Context, run domain.SagaRun, _ domain.Step) (map[string]any, error) {
errInfo, _ := run.Variables["_error"].(map[string]any)
return map[string]any{"recovered": true, "failed_step": errInfo["step_id"]}, nil
}))

sc.Register(domain.WorkflowDefinition{
ID: "checkout", Version: 1, Start: "protect", Published: true,
Steps: []domain.Step{
// The frame: protect "charge", jump to "recover" on any error.
{ID: "protect", Type: "try_catch",
Inputs: map[string]any{"try": []string{"charge"}, "catch": "recover"},
Next: "charge"},
{ID: "charge", Type: "charge_card", Next: "done"},
{ID: "recover", Type: "notify_ops", Next: "done"},
{ID: "done", Type: domain.StepTypeEnd},
},
})

runID, _ := sc.Start(context.Background(), "checkout", nil)
run, _ := sc.Get(context.Background(), runID)
fmt.Println(run.State) // succeeded โ€” the error was caught
fmt.Println(run.Variables["recovered"]) // true

The try_catch step's Next points at the first step inside the protected region; the protected step's Next points past it. See the try_catch verb for the nesting rules, and Testing for asserting both the caught and uncaught (failed) paths.

The Step.Retry and Step.Compensation fields exist on the schema but are not yet executed by the engine โ€” try_catch is the supported recovery mechanism today.