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(), pasrollbackTransaction(). - 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
- Saga Orchestration — contexte d'utilisation principal des compensations.
- Saga : chorégraphie vs orchestration — choix de topologie qui conditionne où les compensations s'exécutent.
- Compensating Action — le pattern parent au sens architectural.
- Idempotent Receiver EDI — propriété indispensable des compensations.
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