Skip to Content
How-To GuidesScalaSaga-Pattern Transactions (Scala)

Saga-Pattern Transactions (Scala)

Overview

Golem supports the saga pattern for multi-step operations where each step has a compensation (undo) action. If a step fails, previously completed steps are automatically compensated in reverse order.

Defining Operations

Each operation has an async execute function and an async compensate function that return Future[Either[Err, Out]].

Critical: Both execute and compensate must return a Future that completes only after the underlying work finishes. If the work involves a JS Promise (e.g., fetch), convert it to a Future via FutureInterop.fromPromise and chain with flatMap/map. Never fire a Promise and ignore the result — doing so makes operation ordering non-deterministic, which breaks compensation ordering.

import golem.{Transactions, FutureInterop} import scala.concurrent.Future import scala.scalajs.js // Correct: awaits the fetch Promise before completing the Future val reserveInventory = Transactions.operation[String, Unit, String]( orderId => { val promise = js.Dynamic.global.fetch( s"http://example.com/orders/$orderId/reserve", js.Dynamic.literal(method = "POST") ).asInstanceOf[js.Promise[js.Dynamic]] FutureInterop.fromPromise(promise).map(_ => Right(())) } )( (orderId, _) => { val promise = js.Dynamic.global.fetch( s"http://example.com/orders/$orderId/reserve", js.Dynamic.literal(method = "DELETE") ).asInstanceOf[js.Promise[js.Dynamic]] FutureInterop.fromPromise(promise).map(_ => Right(())) } )

For synchronous operations, Future.successful is fine:

val incrementCounter = Transactions.operation[Unit, Int, String]( _ => { counter += 1; Future.successful(Right(counter)) } )( (_, oldValue) => { counter = oldValue - 1; Future.successful(Right(())) } )

Fallible Transactions

On failure, compensates completed steps and returns the error:

import golem.Transactions val result: Future[Either[Transactions.TransactionFailure[String], (String, String)]] = Transactions.fallibleTransaction[(String, String), String] { tx => for { reservation <- tx.execute(reserveInventory, "SKU-123") charge <- reservation match { case Right(r) => tx.execute(chargePayment, 4999L).map(_.map(c => (r, c))) case Left(e) => Future.successful(Left(e)) } } yield charge }

Infallible Transactions

On failure, compensates completed steps and retries the entire transaction:

import golem.Transactions val result: Future[Either[Transactions.TransactionFailure[Any], (String, String)]] = Transactions.infallibleTransaction { tx => for { reservation <- tx.execute(reserveInventory, "SKU-123") charge <- tx.execute(chargePayment, 4999L) } yield (reservation, charge) } // Handle Left outcomes for exhaustion, policy error or partial rollback.

Guidelines

  • After each fully compensated failed infallible attempt, resolve the host policy with verb saga, noun the agent URN, and attempt/error-kind properties. next-retry-delay uses the existing evaluator and opaque binary continuation; the SDK uses durable clocks and timers. No match selects the agent default. Unbounded policies are rejected, not capped. Count/time/never exhaustion and selector/evaluator errors return typed failures retaining the original failure. New attempts append history without rewinding.
  • Caught exceptions and failed Futures run compensation in reverse order. Fallible transactions propagate the original exception only after complete rollback; partial rollback retains both original and compensation failures. An uncatchable guest trap cannot run compensation.
  • Keep compensation logic idempotent — it may be called more than once
  • Compensation runs in reverse order of execution
  • Use fallibleTransaction when failure is an acceptable outcome
  • Use infallibleTransaction only when another attempt after compensation is appropriate; eventual success is not guaranteed