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 eventuallyGuidelines
- Keep compensation logic idempotent — it may be called more than once
- Compensation runs in reverse order of execution
- Use
fallibleTransactionwhen failure is an acceptable outcome - Use
infallibleTransactionwhen the operation must eventually succeed