Skip to content

Saga

A Saga is a distributed persistent transaction that uses the Saga Pattern. Sagas are used to coordinate state changes across multiple components (typically entities) in a system. Every change (action) has a compensating action to undo the action. This permits an organized rollback if one component cannot proceed with the transaction.

Signature

A saga's requires and returns may name an existing type, the same as a function's:

saga CheckoutProcess is {
  requires record CheckoutInputs
  returns  record CheckoutOutcome

  step TakePayment is {
    tell command ProcessPayment(orderId) to entity PaymentService
  } reverted by {
    tell command RefundPayment(orderId) to entity PaymentService
  }

  step ReserveStock is {
    tell command ReserveItems(orderId) to entity Inventory
  } reverted by {
    tell command ReleaseItems(orderId) to entity Inventory
  }
} with {
  option is compensate
}

A saga must define at least two steps — a one-step saga has nothing to coordinate — and a step's name must not collide with the message it sends, or the reference becomes ambiguous.

Options

Option Meaning
compensate On failure, run the accumulated steps' undo blocks in reverse
parallel Start all steps at once; the coordinator gathers results asynchronously. Any one failure compensates in reverse order of the original sends.
timeout("30s") How long the whole saga may take
retry(3) How many times to retry a step that fails
undo-retry(2) How many times to retry a compensation that fails
failure-message The message to emit when the saga ultimately fails

A saga is sequential by definition, so parallel declares the exception and there is no sequential option — asking for the default said nothing, so it was dropped in RIDDL 2.0.

retry appears at two scopes and means the same thing at each: on a SagaStep it bounds that step, on the saga it bounds every step. A step's own retry wins for that step; the saga's applies to steps without one. That precedence is a contract between generators, not something riddlc enforces.

Durations must be positive

timeout("0s"), timeout("-1m") and timeout("PT0S") are Errors. A saga bounded by zero has expired before its first step starts, and a negative bound is not describable at all. It is a distinct message from the vague-duration warning, because the two need different fixes: one means "state a unit", the other means "state a magnitude".

These options are contracts for the code generator rather than behavior riddlc enforces — apart from the duration rule above, which it checks.

Scope

A saga stays within one domain

A saga orchestrates a multi-step transaction within one bounded domain. Every reference in a step — send/tell/yield message targets, morph entity and state, become entity and handler, put output, and any embedded call or get — must resolve to a definition within the saga's own enclosing domain.

A reference resolving to a definition owned by a different domain crosses the saga's boundary and is an Error. A referent with no owning domain — a root or shared definition — is allowed.

A saga needs at least two steps

One step is an Error: "Sagas must define at least 2 steps". A single-step transaction has nothing to coordinate and no ordering to compensate in reverse, so it wants a plain handler instead.

One failure point per step

A step's do/undo is all-or-nothing: the compensation assumes all or none of the do-block happened. So a step should have at most one potential failure point, and more than one draws a Warning suggesting the step be split.

send, tell, yield and put can fail, and so can each embedded call or getlet x = call F(get from input I) counts as two.

A saga step may not ask

An ask anywhere in a step is an Error, including one nested inside a larger value expression. A saga must not depend on dynamic state, or the same inputs could yield different transaction results at different times — and compensation would then be reversing something other than what actually happened.

Acquire the value in a handler and pass it into the saga through its requires, so the saga is closed over its inputs and the undo sees the same data the do saw.

A step containing an ask is not also reported for the failure-point rule above: an ask is itself a failure point, so every such step would trip that rule too, and the advice to split the step would not help.

A Saga Is Not a Processor

A Saga extends the vital definition base rather than Processor, so it takes no version and no copyright.

It does bear inlets and outlets — a coordinator has messages to receive and emit — so "not a processor" is about the version and copyright scopes, not about ports. Its contents are exactly: steps, ports, functions and includes.

Occurs In

Contains

flowchart TD
    Saga(["Saga"]) --> Step["Saga Step"]
    Saga --> Inlet
    Saga --> Outlet
    Saga --> Function
    Step --> Statement