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_transactionmacros. 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:
- Execute — perform a step (e.g., reserve inventory, charge payment)
- 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
| API | Purpose |
|---|---|
@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_operationso 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_indexfor infallible (auto-retry) semantics; useResultfor 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_transactionhelpers — check the SDK changelog for updates