ediverse Explorer la plateforme

À la une PEPPOL BIS Billing 3.0 L’obligation européenne d’e-invoicing arrive : France sept 2026, Belgique janv 2026, Allemagne 2025.

Compensating Transactions

Une fois qu'une étape de saga est commitée, on ne peut plus la rollback. Il faut une opération métier explicite qui en annule l'effet observable — un ORDRSP négatif pour défaire une commande, une facture d'avoir pour défaire un INVOIC.

Problème

Dans une transaction ACID, l'abort renvoie l'état à avant tous les writes locaux. Mais une saga distribuée commit chaque étape localement : quand l'étape 5 échoue, les étapes 1-4 sont déjà visibles dans le monde réel (mail envoyé, stock réservé, partenaire notifié). On ne peut pas remonter le temps — il faut compenser : exécuter une nouvelle opération qui annule sémantiquement l'effet observable de l'étape précédente. Le mail envoyé devient un mail d'excuse, la réservation devient une libération, la facture devient un avoir. La difficulté : chaque compensation doit elle-même être fiable, idempotente, et auditée.

Forces

  • Visibilité métier : les compensations sont observables par le partenaire (REMADV négatif, ORDRSP REJECTED) et doivent être contractuellement prévues.
  • Asymétrie temporelle : la compensation arrive après l'étape commitée — l'état intermédiaire est observable, ce qui peut violer des contraintes business si non-géré.
  • Idempotence : une compensation peut être retentée plusieurs fois (le réseau perd des ACK) — elle doit être idempotente par construction.
  • Ordre : les compensations s'exécutent en ordre inverse des étapes (LIFO), pas dans l'ordre original.
  • Compensations partielles : certaines actions sont irreversible (email envoyé, virement bancaire SEPA terminé) — il faut savoir lesquelles avant de concevoir la saga.

Solution

Pour chaque action métier X exécutée dans une saga, définir une action de compensation X⁻¹ qui en annule l'effet observable. La compensation est :

  • Métier, pas technique : cancelOrder(), pas rollbackTransaction().
  • Idempotente : appeler 5 fois cancelOrder() doit avoir le même effet qu'une seule fois.
  • Commutative quand possible : l'ordre d'exécution n'a pas d'importance.
  • Auditée : chaque compensation génère un événement OrderCancelled, StockReleased, etc.
  • Persistée : le saga state liste les étapes commitées et leurs handlers de compensation correspondants.

Structure

Saga forward (succès)
─────────────────────
Step 1: reserveStock      → committed
Step 2: bookShipment       → committed
Step 3: chargeCustomer     → committed
Step 4: sendInvoiceEDI     → committed
                            ↓
                          SUCCESS

Saga forward avec échec à l'étape 4
────────────────────────────────────
Step 1: reserveStock      → committed
Step 2: bookShipment       → committed
Step 3: chargeCustomer     → committed
Step 4: sendInvoiceEDI     → FAILED (partenaire AS4 down depuis 6h)

Saga backward (compensations LIFO)
───────────────────────────────────
Step 3⁻¹: refundCustomer   → committed (avoir SEPA)
Step 2⁻¹: cancelShipment   → committed (transporteur informé)
Step 1⁻¹: releaseStock     → committed (stock remis disponible)
                             ↓
                          COMPENSATED

Implémentation EDI

Sur un flux EDI O2C (Order to Cash) typique :

-- Schema table de saga state
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
);

-- Exemple de compensation : INVOIC déjà émis, à compenser par CRDADV
-- Le partenaire doit recevoir une facture d'avoir EDIFACT D.96A
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'      -- Référence commande originale
RFF+IV:INV-2026-12345'      -- Référence INVOIC initial
... lignes avec quantités négatives ...
UNT+15+1'

Cas particuliers fréquents : (1) Stock déjà expédié — la compensation est une procédure retour (NOTIF Retour) avec délai de plusieurs jours, et la saga doit attendre. (2) Mail envoyé au client — pas de compensation possible, prévoir mail correctif. (3) Virement SEPA déjà transmis — pas de rollback bancaire automatique, prévoir contre-virement SEPA SCT par fichier ISO 20022 pain.001. (4) Production lancée en usine — compensation impossible, saga doit revenir en mode partial success manuel.

Anti-patterns

  • Compensation = DELETE FROM orders WHERE id = ... — la compensation est métier, pas SQL. Garder la trace.
  • Compensations chaînées qui appellent d'autres sagas — récursion infinie possible, debugging cauchemar.
  • Ignorer les irréversibilités physiques — promettre au business que « la saga peut tout rollback » quand elle ne peut pas.
  • Compensation envoyée au partenaire sans accusé — l'annulation EDI doit attendre le CONTRL/AK9 d'acceptation avant que la saga se déclare compensated.
  • Pas de timeout sur la compensation — si l'opération inverse échoue, la saga reste FAILED indéfiniment, intervention manuelle requise.

Patterns liés

Sources

  • Garcia-Molina H., Salem K. — Sagas, ACM SIGMOD Conference 1987. Le papier fondateur, qui introduit explicitement les 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. La justification théorique de pourquoi les compensations remplacent 2PC à grande échelle.
  • UN/EDIFACT Directory D.96A — CRDADV (Credit Advice) Message Reference. unece.org/trade/uncefact/unedifact