Sagas
Which step of a saga must come last?
Two constraints can make prepare unavailable, and the literature stresses the wrong one.
- Duration. This is the textbook motivation (Garcia-Molina and Salem, 1987). A transaction that spans hours, or waits for human decisions, cannot hold locks, because W in the conflict formula would be ruinous. True, but secondary.
- Heterogeneity. This is the main modern reason. A payment gateway, a shipping API or an email service does not export a prepare phase at all. You cannot run 2PC against a participant that has no concept of "vote yes and wait".
Sagas exist mostly because participants do not offer prepare, not because transactions are long. This changes when you should reach for one. Kestrel's card gateway is the example.
The mechanism
Split the transaction into local transactions T₁ … Tₙ. Each has a compensator C₁ … Cₙ (an action that semantically undoes it: "refund", "release the seat"). Run forward. If step k fails, run C_{k−1} … C₁ backwards.
What you give up
Most texts say this too gently. Atomicity is gone, and isolation was never there. Intermediate states are visible to everyone. Between T₂ and C₂, another actor can see T₂'s effect and act on it, and the compensation cannot reach that actor (for example: the seat shows as "sold" for a minute, a different service reads it, and then your saga releases it). The usual mitigation is a semantic lock: a "pending" status on the record, so other actors know not to trust it. That is locking re-implemented in application code, without a lock manager, without deadlock detection, and without anyone being obliged to honour it.
Four requirements for compensators
The last two are where sagas actually fail.
- They must be idempotent, because they will be retried.
- They must tolerate out-of-order arrival (the "cancel" message can arrive before the "do" message).
- They must be guaranteed to succeed eventually. A compensation that can fail leaves the saga stuck and needs a compensation of its own, which is a regress that stops only at a step that cannot fail. In every real system, that step is "put it in a queue for a human".
- They must exist at all. You cannot un-send an email, un-disclose data, or un-ship a package.
The most useful design rule: order the irreversible step last
That last requirement gives the single most useful saga rule, and it is "atomicity over n items is atomicity over one item" in disguise. Put the irreversible step at the end. If the step that cannot be undone happens last, it becomes the designated event that decides whether everything before it counts. You have recovered a commit point by construction instead of by protocol. Where that is impossible, defer the irreversible effect (hold the email; authorize the card payment but do not settle it) until a point that can serve as the commit.
For Kestrel: reserve the seat (reversible, compensator = release), authorize then capture the payment (compensator = refund, which exists but is slow and costs fees), and send the e-ticket last (it cannot be undone).
Orchestration versus choreography
- An orchestrator keeps the saga's state machine in one component. "Where is this saga stuck?" is answered by looking in one place. The price: a component that everything depends on.
- Choreography spreads the state machine into event reactions: each service listens for events and does its step. The same question now requires rebuilding the saga from logs of several services.
This is an observation, not a law: teams that choose choreography for decoupling often rebuild an orchestrator later, to answer operational questions. Durable-execution engines (systems that checkpoint a workflow's own state, so that it resumes after a crash) are the current form of the orchestrator.