ediverse Explore the platform

Spotlight PEPPOL BIS Billing 3.0 The EU e-invoicing mandate is here — France Sept 2026, Belgium Jan 2026, Germany 2025.

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(), not rollbackTransaction().
  • 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, StockReleased event.
  • 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.

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