Compensating Transactions
Once a saga step is committed, you cannot roll it back. You need an explicit business operation that undoes its observable effect — a negative ORDRSP to cancel an order, a credit note to offset an INVOIC.
Problem
In an ACID transaction, abort returns state to before all local writes. But a distributed saga commits each step locally: when step 5 fails, steps 1-4 are already visible to the outside world (email sent, stock reserved, partner notified). You cannot rewind time — you must compensate: run a new operation that semantically undoes the observable effect of the previous step. The sent email becomes an apology, the reservation becomes a release, the invoice becomes a credit note. The hard part: each compensation must itself be reliable, idempotent, and audited.
Forces
- Business visibility: compensations are visible to the partner (negative REMADV, ORDRSP REJECTED) and must be contractually anticipated.
- Temporal asymmetry: compensation arrives after the committed step — the intermediate state is observable, which may violate business constraints if unmanaged.
- Idempotency: a compensation may be retried multiple times (network loses ACKs) — must be idempotent by design.
- Ordering: compensations run in reverse order of steps (LIFO), not the original order.
- Partial compensations: some actions are irreversible (email sent, completed SEPA transfer) — you must know which ones before designing the saga.
Solution
For each business action X executed in a saga, define a compensating
action X⁻¹ that undoes its observable effect. The compensation is:
- Business, not technical:
cancelOrder(), notrollbackTransaction(). - Idempotent: calling
cancelOrder()5 times has the same effect as once. - Commutative when possible: execution order does not matter.
- Audited: each compensation produces a
OrderCancelled,StockReleasedevent. - Persisted: saga state lists committed steps and their corresponding compensation handlers.
Structure
Saga forward (success)
─────────────────────
Step 1: reserveStock → committed
Step 2: bookShipment → committed
Step 3: chargeCustomer → committed
Step 4: sendInvoiceEDI → committed
↓
SUCCESS
Saga forward with failure at step 4
────────────────────────────────────
Step 1: reserveStock → committed
Step 2: bookShipment → committed
Step 3: chargeCustomer → committed
Step 4: sendInvoiceEDI → FAILED (AS4 partner down 6h)
Saga backward (compensations LIFO)
───────────────────────────────────
Step 3⁻¹: refundCustomer → committed (SEPA credit)
Step 2⁻¹: cancelShipment → committed (carrier informed)
Step 1⁻¹: releaseStock → committed (stock back to available)
↓
COMPENSATED EDI implementation
On a typical O2C (Order to Cash) EDI flow:
-- Saga state table schema
CREATE TABLE saga_state (
saga_id UUID PRIMARY KEY,
saga_type VARCHAR(80), -- 'OrderToInvoice'
state JSONB, -- step name → 'committed' | 'compensated'
current_step VARCHAR(80),
status VARCHAR(20), -- RUNNING|FAILED|COMPENSATING|DONE
failed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ
);
-- Example: INVOIC already sent, to compensate with CRDADV
-- The partner must receive an EDIFACT D.96A credit note
UNH+1+CRDADV:D:96A:UN:EAN006'
BGM+381+CR-2026-12345+9' -- Document type 381 = Credit note
DTM+137:20260518:102'
RFF+ON:ORD-2026-12345' -- Original order reference
RFF+IV:INV-2026-12345' -- Initial INVOIC reference
... lines with negative quantities ...
UNT+15+1' Frequent edge cases: (1) Stock already shipped — compensation is a return procedure (NOTIF Return) with multi-day delay, the saga must wait. (2) Email already sent to customer — no compensation possible, plan a corrective email. (3) SEPA wire already submitted — no automatic banking rollback, plan a counter-SEPA SCT via ISO 20022 pain.001 file. (4) Factory production already launched — compensation impossible, saga must revert to manual partial success mode.
Anti-patterns
- Compensation =
DELETE FROM orders WHERE id = ...— compensation is business, not SQL. Keep the trace. - Chained compensations that call other sagas — infinite recursion possible, debugging nightmare.
- Ignoring physical irreversibilities — promising the business that "saga can roll back anything" when it cannot.
- Compensation sent to partner without acknowledgement — EDI cancellation must wait for CONTRL/AK9 acceptance before saga declares itself compensated.
- No timeout on compensation — if the inverse operation fails, saga stays FAILED indefinitely, manual intervention required.
Related patterns
- Saga Orchestration — main usage context for compensations.
- Saga: choreography vs orchestration — topology choice that determines where compensations run.
- Compensating Action — the parent pattern at the architectural sense.
- Idempotent Receiver EDI — essential property of compensations.
Sources
- Garcia-Molina H., Salem K. — Sagas, ACM SIGMOD Conference 1987. The founding paper, explicitly introducing compensating transactions. cs.cornell.edu
- Richardson C. — Microservices Patterns, Manning 2018, §4.3 ("Designing compensating transactions").
- Microsoft Cloud Adoption Framework — Compensating Transaction pattern. learn.microsoft.com
- Pat Helland — Life Beyond Distributed Transactions: An Apostate's Opinion, ACM Queue 2007. The theoretical justification for why compensations replace 2PC at scale.
- UN/EDIFACT Directory D.96A — CRDADV (Credit Advice) Message Reference. unece.org/trade/uncefact/unedifact