Welcome to the new Golem Cloud Docs! 👋
Saga-Pattern Transactions (MoonBit)

Saga-Pattern Transactions (MoonBit)

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.

SDK Limitation: The MoonBit SDK does not yet provide a high-level transaction/saga API like Rust's fallible_transaction / infallible_transaction macros. A future SDK release may add dedicated transaction support. In the meantime, the saga pattern can be implemented manually using the oplog and atomic operation APIs.

Concept

A saga transaction is a sequence of execute + compensate pairs:

  1. Execute — perform a step (e.g., reserve inventory, charge payment)
  2. Compensate — undo that step (e.g., cancel reservation, refund payment)

If step N fails, compensations for steps N-1 through 1 run in reverse order. Compensation logic must be idempotent — it may be called more than once during retries.

Manual Implementation

Use Golem's oplog and atomic operation APIs from @golem_sdk/api to build saga-style transactions manually.

Available APIs

APIPurpose
@api.mark_begin_operation()Start an atomic region; returns an oplog index
@api.mark_end_operation(idx)Commit an atomic region
@api.with_atomic_operation(f)Run f inside an atomic region (auto-committed)
@api.set_oplog_index(idx)Roll back execution to a previous oplog position
@api.get_oplog_index()Get the current oplog position

Defining Operations

Model each step as a pair of functions — one to execute and one to compensate:

fn reserve_inventory(sku : String) -> String {
  // Call inventory API, return reservation_id
  let reservation_id = call_inventory_api(sku)
  reservation_id
}

fn cancel_reservation(reservation_id : String) -> Unit {
  // Compensate: cancel the reservation (must be idempotent)
  call_cancel_reservation_api(reservation_id)
}

fn charge_payment(amount : UInt) -> String {
  // Call payment API, return charge_id
  let charge_id = call_payment_api(amount)
  charge_id
}

fn refund_payment(charge_id : String) -> Unit {
  // Compensate: refund the payment (must be idempotent)
  call_refund_api(charge_id)
}

Fallible Transaction (Manual)

On failure, compensate completed steps in reverse order and return an error:

struct CompletedStep {
  compensate : () -> Unit
}

fn fallible_saga() -> Result[String, String] {
  let completed : Array[CompletedStep] = []

  // Step 1: Reserve inventory
  let reservation_id = try {
    @api.with_atomic_operation(fn() { reserve_inventory("SKU-123") })
  } catch {
    e => {
      compensate_all(completed)
      return Err("Reserve failed: \{e}")
    }
  }
  completed.push({ compensate: fn() { cancel_reservation(reservation_id) } })

  // Step 2: Charge payment
  let charge_id = try {
    @api.with_atomic_operation(fn() { charge_payment(4999) })
  } catch {
    e => {
      compensate_all(completed)
      return Err("Payment failed: \{e}")
    }
  }
  completed.push({ compensate: fn() { refund_payment(charge_id) } })

  Ok("reservation=\{reservation_id}, charge=\{charge_id}")
}

fn compensate_all(steps : Array[CompletedStep]) -> Unit {
  // Compensate in reverse order
  for i = steps.length() - 1; i >= 0; i = i - 1 {
    (steps[i].compensate)()
  }
}

Infallible Transaction (Manual)

On failure, compensate completed steps and retry the entire transaction using set_oplog_index:

fn infallible_saga() -> String {
  let checkpoint = @api.get_oplog_index()
  let completed : Array[CompletedStep] = []

  // Step 1: Reserve inventory
  let reservation_id = try {
    @api.with_atomic_operation(fn() { reserve_inventory("SKU-123") })
  } catch {
    _ => {
      compensate_all(completed)
      @api.set_oplog_index(checkpoint) // retry from the beginning
      panic() // unreachable — set_oplog_index rewinds execution
    }
  }
  completed.push({ compensate: fn() { cancel_reservation(reservation_id) } })

  // Step 2: Charge payment
  let charge_id = try {
    @api.with_atomic_operation(fn() { charge_payment(4999) })
  } catch {
    _ => {
      compensate_all(completed)
      @api.set_oplog_index(checkpoint) // retry from the beginning
      panic() // unreachable
    }
  }
  completed.push({ compensate: fn() { refund_payment(charge_id) } })

  "reservation=\{reservation_id}, charge=\{charge_id}"
}

When set_oplog_index is called, Golem rewinds execution to the saved checkpoint. The side-effecting calls will be re-executed on retry, potentially with different results.

Guidelines

  • No high-level API yet — implement sagas manually using oplog primitives as shown above
  • Wrap each step in with_atomic_operation so partial steps are retried as a unit on failure
  • Keep compensation logic idempotent — it may run more than once
  • Compensate in reverse order of execution
  • Use set_oplog_index for infallible (auto-retry) semantics; use Result for fallible semantics
  • Side-effecting calls (HTTP, database) should be wrapped in durable function patterns for replay safety
  • A future MoonBit SDK release may add fallible_transaction / infallible_transaction helpers — check the SDK changelog for updates