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[(String, String)] = Transactions.infallibleTransaction { tx => for { reservation <- tx.execute(reserveInventory, "SKU-123") charge <- tx.execute(chargePayment, 4999L) } yield (reservation, charge) } // Always succeeds eventually

Guidelines

  • 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 when the operation must eventually succeed
Last updated on