Sagas et intégration¶
Les transactions distribuées n'existent pas — les sagas, l'outbox et l'idempotence sont les patterns qui garantissent la cohérence entre services.
Le problème des transactions distribuées¶
Dans un monolithe, une transaction SQL enveloppe plusieurs écritures : tout reussit ou tout est annule. Dans un système distribué, chaque service a sa propre base. Le commit distribué (2PC) est lent, fragile et bloque les ressources. En pratique, on utilise des sagas.
graph LR
subgraph "Monolithe"
TX["Transaction ACID<br/>BEGIN → INSERT orders → INSERT payments → COMMIT"]
end
subgraph "Distribue"
S1[Order Service<br/>DB Orders]
S2[Payment Service<br/>DB Payments]
S3[Inventory Service<br/>DB Inventory]
S1 --"event"--> S2
S2 --"event"--> S3
end Le 2PC (Two-Phase Commit) coordonné un commit global : le coordinateur demande à chaque participant de préparer (phase 1), puis de committer (phase 2). Si un participant échoué, tous annulent. Le problème : si le coordinateur crashe entre les deux phases, les participants restent bloques indéfiniment (problème du coordinateur defaillant). C'est pourquoi les microservices évitent le 2PC.
Saga choreography¶
Chaque service emet un événement après son action. Les autres services réagissent et emettent le suivant. En cas d'échec, des événements de compensation sont emis pour annuler les étapes précédentes.
sequenceDiagram
participant O as Order Service
participant P as Payment Service
participant I as Inventory Service
participant N as Notification Service
O->>O: create order (pending)
O-->>P: OrderCreated
P->>P: charge payment
P-->>I: PaymentSucceeded
I->>I: reserve stock
I-->>N: StockReserved
N->>N: send confirmation
Note over P,I: En cas d'echec du paiement
P-->>O: PaymentFailed
O->>O: cancel order (compensation) Avantages de la choreography¶
- Couplage faible : chaque service ne connait que les événements qu'il emet et ceux qu'il écoute
- Autonomie : chaque équipe géré son service et ses réactions sans coordonner avec un orchestrateur
- Scalabilité : chaque service scale independamment
Limites de la choreography¶
- Visibilité : le flux complet est distribué dans le code de chaque service — difficile à reconstituer
- Debugging : tracer une saga complète nécessité un correlation ID et un outil de tracing distribué
- Cycles : le risque de créer des boucles d'événements augmente avec le nombre de services
Saga orchestration¶
Un orchestrateur central connait le workflow et appelle chaque service dans l'ordre. Il géré les compensations de manière centralisee.
sequenceDiagram
participant Orch as Orchestrateur
participant O as Order Service
participant P as Payment Service
participant I as Inventory Service
Orch->>O: createOrder
O-->>Orch: OrderCreated
Orch->>P: chargePayment
P-->>Orch: PaymentSucceeded
Orch->>I: reserveStock
I-->>Orch: StockReserved
Note over Orch,I: En cas d'echec de la reservation
I-->>Orch: StockReservationFailed
Orch->>P: refundPayment (compensation)
P-->>Orch: PaymentRefunded
Orch->>O: cancelOrder (compensation) Le coordinateur de saga¶
L'orchestrateur est une machine a états qui persiste la progression de la saga. À chaque étape, il :
- Enregistre l'état courant en base
- Appelle le service suivant
- Attend la réponse (ou timeout)
- En cas de succès, passe à l'étape suivante
- En cas d'échec, déclenche les compensations dans l'ordre inverse
stateDiagram-v2
[*] --> OrderCreated
OrderCreated --> PaymentCharged: payment success
OrderCreated --> OrderCancelled: payment failed
PaymentCharged --> StockReserved: stock success
PaymentCharged --> PaymentRefunded: stock failed
PaymentRefunded --> OrderCancelled: compensation done
StockReserved --> Confirmed: all steps done
OrderCancelled --> [*]
Confirmed --> [*] Choreography vs orchestration¶
| Propriété | Choreography | Orchestration |
|---|---|---|
| Couplage | Faible (via événements) | Fort sur l'orchestrateur |
| Visibilité | Difficile (flux distribué) | Claire (logique centralisee) |
| Testabilité | Complexe (tester chaque réaction) | Simple (tester l'orchestrateur) |
| Scalabilité | Bonne (chaque service indépendant) | Goulot potentiel sur l'orchestrateur |
| Maintenance | Répartie dans chaque service | Centralisee dans l'orchestrateur |
| Cas d'usage | Flux simples, 2-3 étapes | Flux complexes, logique critique |
Recommandation : choreography pour les flux simples avec peu d'étapes et des équipes autonomes. Orchestration pour les flux métier complexes ou la visibilité et le contrôle sont critiques. La majorité des systèmes utilisent les deux selon les cas.
Compensating transactions¶
Les compensations sont les "rollbacks" du monde distribué. Contrairement a un rollback SQL qui annule physiquement l'écriture, une compensation est une nouvelle action qui annule semantiquement l'effet de l'action précédente.
Règles des compensations¶
| Règle | Explication |
|---|---|
| Idempotence | La compensation peut être exécutée plusieurs fois sans effet |
| Commutativite | L'ordre des compensations ne doit pas impacter le résultat final |
| Pas de rollback physique | On annule l'effet, pas l'écriture (credit après débit) |
| Persistance | L'intention de compenser doit être persistee avant exécution |
Exemples concrets¶
| Action originale | Compensation |
|---|---|
| Debiter le compte | Crediter le même montant |
| Réserver le stock | Libérer la quantite réservée |
| Envoyer la commande | Créer un retour / annulation aupres du carrier |
| Envoyer un email de confirmation | Envoyer un email d'annulation |
Actions non compensables¶
Certaines actions ne peuvent pas être compensees :
- Un email envoye ne peut pas être "desenvoye"
- Un SMS facture ne peut pas être rembourse par le système
- Une notification push affichee ne peut pas être retiree
Pour ces cas, on retarde l'exécution : l'email de confirmation est envoye uniquement quand toutes les étapes de la saga sont confirmees. C'est le pattern "pivot transaction" — l'action irreversible est la dernière étape.
Outbox pattern¶
Le problème : écrire en base et publier un événement sont deux opérations. Si le service crashe entre les deux, l'une reussit et pas l'autre — inconsistance garantie.
La solution : écrire les deux dans la même transaction base de données, puis publier séparément.
graph LR
subgraph "Transaction atomique"
DB[(Base principale)]
OB[(Table outbox)]
end
P[Poller / CDC]
MB[Message Broker]
C[Consumers]
DB -->|"meme transaction"| OB
OB -->|"lecture periodique"| P
P -->|"publish"| MB
MB --> C
P -->|"marquer publie"| OB Implémentation¶
- Dans la transaction métier : inserer la donnée + inserer l'événement dans la table
outbox(status=pending) - Un poller (ou CDC avec Debezium) lit les lignes pending et les publie sur le broker
- Une fois publie avec succès, marquer la ligne comme done
-- Transaction atomique
BEGIN;
INSERT INTO orders (id, customer_id, status)
VALUES ('ORD-001', 'USR-001', 'pending');
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload)
VALUES (
'EVT-001',
'Order',
'ORD-001',
'OrderCreated',
'{"orderId": "ORD-001", "customerId": "USR-001"}'
);
COMMIT;
Poller vs CDC¶
| Approche | Mécanisme | Latence | Complexité |
|---|---|---|---|
| Poller | SELECT périodique sur la table outbox | Secondes | Faible |
| CDC (Debezium) | Lecture du WAL de la base | Millisecondes | Moyenne |
Le CDC avec Debezium est preferable pour les systèmes a fort volume : il lit le Write-Ahead Log de PostgreSQL et publie les changements directement sur Kafka, sans polling.
Idempotence¶
Toute opération distribuée doit pouvoir être rejouee sans effet de bord. Les retries sont inevitables — réseau instable, timeout, crash du consumer après traitement mais avant ack.
Idempotency key¶
Le client généré une clé unique (UUID v4) et l'inclut dans chaque requête. Le serveur stocke la clé et le résultat. Si la même clé arrive une seconde fois, il retourné le résultat précédent sans retraiter.
POST /payments
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Content-Type: application/json
{"order_id": "ORD-1", "amount": 4990}
Règles d'implémentation¶
- Stocker
(idempotency_key, result, created_at)en base avec un index unique sur la clé - Retourner exactement la même réponse pour une clé déjà vue
- Ne pas traiter deux requêtes avec la même clé en parallèle (verrou optimiste ou transactionnel)
- Définir une durée de rétention des clés (24h a 7j selon le contexte)
- Documenter la politique d'idempotence dans le contrat d'API
Idempotence naturelle¶
Certaines opérations sont naturellement idempotentes :
| Opération | Idempotente ? | Explication |
|---|---|---|
PUT /users/1 {name} | Oui | Écraser avec la même valeur = même état |
DELETE /users/1 | Oui | Supprimer un élément déjà supprimé = OK |
POST /payments | Non | Chaque appel crée un nouveau paiement |
PATCH /stock -1 | Non | Chaque appel décrémente |
Pour les opérations non idempotentes, l'idempotency key est obligatoire.
Dead letter queues¶
Quand un message ne peut pas être traite après N tentatives, il est déplacé vers une Dead Letter Queue (DLQ). La DLQ préservé le message pour analyse sans bloquer le flux principal.
graph LR
Q[Queue principale] --"deliver"--> C[Consumer]
C --"traitement OK"--> ACK[ack]
C --"echec apres N retries"--> DLQ[Dead Letter Queue]
DLQ --"analyse manuelle"--> OPS[Ops / Alert]
OPS --"correction + replay"--> Q Politique de retry¶
Avant d'envoyer en DLQ, le consumer tente de retraiter le message avec une stratégie de backoff :
| Stratégie | Délai entre retries | Cas d'usage |
|---|---|---|
| Fixed | 5s, 5s, 5s | Erreurs transitoires simples |
| Exponential backoff | 1s, 2s, 4s, 8s, 16s | Service temporairement down |
| Exponential + jitter | 1s±rand, 2s±rand, 4s±rand | Éviter le thundering herd |
Gestion des DLQ¶
Les messages en DLQ ne doivent pas être ignores. Un processus opérationnel doit les traiter :
- Alerter : une métrique sur le nombre de messages en DLQ déclenche une alerte
- Analyser : examiner le message et l'erreur. Bug applicatif ? Message corrompu ? Service down ?
- Corriger : déployer le fix ou corriger les données
- Rejouer : remettre le message dans la queue principale
- Archiver : si le message est irreparable, l'archiver avec une justification
Poison messages¶
Un poison message est un message qui fait crasher le consumer à chaque tentative, quelle que soit la correction. Sans DLQ, ce message bloque indéfiniment la queue. Avec DLQ, il est isole après N tentatives.
Causes courantes : format invalide, donnée corrompue, incompatibilite de version du schéma, référence a une entité supprimée.
DLQ = dette technique visible
Une DLQ qui grossit est un signal d'alarme. Ce n'est pas un poubelle — c'est un backlog de problèmes a résoudre. Monitorer le taux de messages en DLQ et le temps moyen de résolution. Si la DLQ grossit plus vite qu'on ne la vide, le système a un problème structurel.
Patterns complementaires¶
Transactional outbox + saga¶
Combiner l'outbox pattern avec les sagas : chaque étape de la saga utilise l'outbox pour garantir l'émission de l'événement. L'orchestrateur lit les événements de réponse via le broker.
sequenceDiagram
participant Orch as Orchestrateur
participant OS as Order Service
participant OB as Outbox
participant MB as Message Broker
Orch->>OS: createOrder
OS->>OS: INSERT order + INSERT outbox (meme TX)
OS-->>Orch: 200 OK
OB->>MB: poller publie OrderCreated
MB-->>Orch: OrderCreated event Inbox pattern¶
Le pendant de l'outbox côté consumer. Le consumer insere le message reçu dans une table inbox (avec deduplication sur le message ID) dans la même transaction que le traitement métier. Ca garantit l'idempotence et l'atomicite de la consommation.
BEGIN;
-- deduplication
INSERT INTO inbox (message_id, received_at)
VALUES ('MSG-001', NOW())
ON CONFLICT (message_id) DO NOTHING;
-- si insert reussit (nouveau message), traiter
-- si conflict (message deja vu), ne rien faire
UPDATE inventory SET quantity = quantity - 1
WHERE product_id = 'PROD-001';
COMMIT;
Claim check pattern¶
Quand le payload du message est trop volumineux pour le broker (images, documents, exports), on stocke le payload dans un object store (S3) et on publie uniquement une référence (claim check) sur le broker.
graph LR
P[Producer] --"1. upload payload"--> S3[(Object Store)]
S3 --"2. retourne URL"--> P
P --"3. publie claim check"--> MB[Broker]
MB --"4. deliver"--> C[Consumer]
C --"5. telecharge payload"--> S3 Ce pattern evite de saturer le broker et permet de gérer des payloads de taille arbitraire.
Commencer simple
Ne déployer que les patterns nécessaires. Un outbox + idempotency key couvre 80% des besoins de cohérence distribuée. Les sagas orchestrees et les DLQ viennent quand les flux deviennent complexes. La sophistication du pattern doit correspondre à la complexité du problème — pas à l'ambition de l'architecte.
Chapitre suivant : Persistance distribuee — polyglot persistence, théorème CAP et partitionnement à l'échelle.