Saga-Pattern Transactions (Go)
Overview
Golem supports the saga pattern for multi-step operations where each step has a compensation (undo). If a later step fails, the steps that already succeeded are compensated in reverse order. The saga helpers build on the durability primitives (an atomic region + the oplog) — no persistence beyond what the runtime already provides.
The Go API lives in the transaction package (github.com/golemcloud/golem/sdks/go/golem/transaction):
transaction.Operation[In, Out, E]— a compensable step, built withtransaction.NewOperation.tx.Step(op, input)— run an operation inside a transaction.transaction.Fallible[Out, E](...)— rolls back on failure and returns atransaction.Failure[E].transaction.Infallible[Out, E](...)— rolls back and retries the whole transaction until it succeeds.
Expected step failures are modeled as the error arm of a golem.Result (type
E, often string). A panic is an infra failure: it traps the component at once
and leaves the atomic region open, so the runtime replays it.
Steps
- Define each operation with
transaction.NewOperation(execute, compensate). - Choose a transaction flavor — fallible (failure is acceptable) or infallible (must eventually succeed).
- Run the steps with
tx.Step(op, input)inside the transaction body. - Handle the outcome — inspect the
golem.Result(fallible) or use the returned value (infallible).
Defining Operations
An operation pairs an execute action with the compensate that undoes it.
execute is func(In) golem.Result[Out, E]; compensate receives both the
original input and the output execute produced, and returns
golem.Result[golem.Unit, E]. Compensation runs only if a later step fails.
import (
"github.com/golemcloud/golem/sdks/go/golem"
"github.com/golemcloud/golem/sdks/go/golem/transaction"
)
// Reserve inventory; compensation cancels the reservation. E is string here.
var reserveInventory = transaction.NewOperation(
func(sku string) golem.Result[string, string] {
reservationID, err := callInventoryAPI(sku)
if err != nil {
return golem.Err[string, string](err.Error())
}
return golem.Ok[string, string](reservationID)
},
func(sku, reservationID string) golem.Result[golem.Unit, string] {
if err := cancelReservation(reservationID); err != nil {
return golem.Err[golem.Unit, string](err.Error())
}
return golem.Ok[golem.Unit, string](golem.Unit{})
},
)
var chargePayment = transaction.NewOperation(
func(amount int64) golem.Result[string, string] {
chargeID, err := callPaymentAPI(amount)
if err != nil {
return golem.Err[string, string](err.Error())
}
return golem.Ok[string, string](chargeID)
},
func(amount int64, chargeID string) golem.Result[golem.Unit, string] {
if err := refundPayment(chargeID); err != nil {
return golem.Err[golem.Unit, string](err.Error())
}
return golem.Ok[golem.Unit, string](golem.Unit{})
},
)Fallible Transactions
On a step failure, the completed steps are compensated (best-effort, in reverse)
and a transaction.Failure[E] is returned — there is no retry. Run steps
with tx.Step(op, input); return an error result from the body to trigger
rollback.
result := transaction.Fallible(func(tx *transaction.Tx[string]) golem.Result[string, string] {
r1 := tx.Step(reserveInventory, "SKU-123")
if r1.IsErr() {
return golem.Err[string, string](r1.Err())
}
r2 := tx.Step(chargePayment, 4999)
if r2.IsErr() {
return golem.Err[string, string](r2.Err())
}
// Combine the two outputs into the transaction's result value.
return golem.Ok[string, string](r1.Ok() + ":" + r2.Ok())
})
if result.IsErr() {
f := result.Err() // transaction.Failure[string]
_ = f.Error // the step failure that aborted the transaction
if !f.RolledBackFully() {
// A compensation itself failed — some effects may remain (needs attention).
_ = f.CompensationFailure // golem.Option[string], Some(...) here
}
} else {
orderID := result.Ok()
_ = orderID
}transaction.Failure[E] fields:
Error E— the step failure that aborted the transaction.CompensationFailure golem.Option[E]—Some(...)if a compensation itself failed during rollback (a partial rollback);Noneif every compensation succeeded.RolledBackFully() bool— true when the rollback was complete.
Infallible Transactions
On a step failure, the completed steps are compensated in reverse, the oplog is
rewound to the transaction start, and the runtime replays the whole
transaction — so it returns the plain Out value, always succeeding eventually:
orderID := transaction.Infallible(func(tx *transaction.Tx[string]) golem.Result[string, string] {
r1 := tx.Step(reserveInventory, "SKU-123")
if r1.IsErr() {
return golem.Err[string, string](r1.Err())
}
r2 := tx.Step(chargePayment, 4999)
if r2.IsErr() {
return golem.Err[string, string](r2.Err())
}
return golem.Ok[string, string](r1.Ok() + ":" + r2.Ok())
})
// orderID is a plain string — the transaction retried until it succeeded.Use transaction.Infallible only when the steps are expected to eventually
succeed — a deterministic failure would retry forever. If a compensation itself
fails during a rollback-for-retry, the transaction cannot be made consistent and
the helper panics (surfacing which compensation failed).
Not available in the Go SDK
The Rust SDK splits operations into sync_operation and operation (async), and
its transactions are async and require wrapping bodies in boxed(async move { … }). The Go SDK has none of that — there is a single transaction.NewOperation, and
transaction bodies are plain synchronous functions. Blocking work inside a step
(HTTP, RPC, promises) suspends the fiber at its await point, so no async/boxed
plumbing is needed. A step is a method on the transaction, tx.Step(op, input),
so the error type comes from the transaction itself.
Guidelines
- Model expected step failures as the error arm of the step’s
golem.Result; reservepanicfor infra failures (it leaves the region open for replay). - Run every step with
tx.Step(op, input)— that is what records the compensation to run on rollback. - Keep compensation logic idempotent — it may run more than once (especially
under
transaction.Infallible). - Compensation runs in reverse order of execution.
- All steps in one transaction share the error type
E(commonlystring). - Use
transaction.Falliblewhen failure is an acceptable outcome;transaction.Infalliblewhen the operation must eventually succeed. - A transaction opens a worker-global atomic region — keep the whole transaction on one logical flow; don’t run concurrent goroutines that perform durable side effects while it is open.
Related Guides
| Guide | Description |
|---|---|
golem-atomic-block-go | The atomic-region / durability primitives transactions build on |
golem-call-another-agent-go | Steps that call other agents via RPC |
golem-make-http-request-go | Steps that call external HTTP APIs |
golem-retry-policies-go | Tune how failing steps/operations are retried |