🔝 Retour au Sommaire
Dans le chapitre précédent, nous avons vu que l'approche Database per Service offre de nombreux avantages : autonomie des équipes, isolation des pannes, scalabilité indépendante. Cependant, elle introduit un défi majeur : comment garantir la cohérence des données lorsqu'une opération métier implique plusieurs services ?
Avec une base de données unique, une simple transaction SQL suffit. Mais quand les données sont réparties sur plusieurs bases, les transactions classiques ne fonctionnent plus. C'est là qu'interviennent les transactions distribuées et, plus particulièrement, le pattern Saga.
Ce chapitre vous expliquera pourquoi les transactions traditionnelles échouent dans un contexte distribué, et comment le pattern Saga permet de maintenir la cohérence des données de manière élégante et résiliente.
Avant d'aborder les transactions distribuées, rappelons le fonctionnement des transactions dans PostgreSQL.
Une transaction PostgreSQL garantit quatre propriétés fondamentales :
| Propriété | Signification |
|---|---|
| Atomicité | Tout ou rien : soit toutes les opérations réussissent, soit aucune n'est appliquée |
| Cohérence | La base passe d'un état valide à un autre état valide |
| Isolation | Les transactions concurrentes ne s'interfèrent pas |
| Durabilité | Une fois validée, la transaction survit aux pannes |
Imaginons une commande e-commerce simple avec une seule base de données :
BEGIN;
-- 1. Créer la commande
INSERT INTO orders (id, user_id, total, status)
VALUES (1001, 42, 150.00, 'pending');
-- 2. Ajouter les articles
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (1001, 501, 2, 75.00);
-- 3. Déduire le stock
UPDATE products
SET stock = stock - 2
WHERE id = 501;
-- 4. Débiter le compte client
UPDATE user_accounts
SET balance = balance - 150.00
WHERE user_id = 42;
COMMIT;Si une erreur survient à l'étape 4 (solde insuffisant), PostgreSQL annule automatiquement toutes les opérations précédentes grâce au ROLLBACK. Le stock n'est pas déduit, la commande n'est pas créée. C'est l'atomicité en action.
Maintenant, imaginons la même opération dans une architecture microservices avec Database per Service :
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │ │ Service │ │ Service │
│ Commandes │ │ Catalogue │ │ Paiements │ │ Stock │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│orders_db│ │catalog_ │ │payments_│ │stock_db │
│ │ │ db │ │ db │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Chaque service a sa propre base PostgreSQL. Il est impossible d'écrire une seule transaction SQL qui englobe les quatre bases.
Voici ce qui peut mal se passer :
- ✅ Service Commandes → Crée la commande (succès)
- ✅ Service Stock → Réserve le stock (succès)
- ✅ Service Paiements → Débite le client (succès)
- ❌ Service Livraison → Échec (transporteur indisponible)
Résultat : Le client est débité, le stock est réservé, mais la livraison n'est pas planifiée. Le système est dans un état incohérent.
Sans mécanisme approprié, il faudrait manuellement annuler les opérations des étapes 1, 2 et 3. C'est exactement le problème que résolvent les transactions distribuées.
Il existe plusieurs stratégies pour gérer les transactions distribuées. Examinons-les avant de nous concentrer sur le pattern Saga.
Le Two-Phase Commit est un protocole classique qui coordonne les transactions entre plusieurs bases de données.
Coordinateur
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Base 1 │ │ Base 2 │ │ Base 3 │
└─────────┘ └─────────┘ └─────────┘
Phase 1 (Prepare) : Le coordinateur demande à chaque base
si elle peut valider la transaction.
Phase 2 (Commit) : Si toutes répondent "oui", le coordinateur
ordonne la validation. Sinon, rollback général.
Bien que PostgreSQL supporte 2PC via PREPARE TRANSACTION, cette approche présente des inconvénients majeurs dans une architecture microservices :
| Problème | Description |
|---|---|
| Verrouillage prolongé | Les ressources sont bloquées pendant toute la durée du protocole |
| Point de défaillance unique | Si le coordinateur tombe en panne, les transactions restent en suspens |
| Latence élevée | Deux allers-retours réseau nécessaires |
| Couplage fort | Tous les participants doivent être disponibles simultanément |
| Non adapté au cloud | Mal compatible avec les environnements élastiques et éphémères |
En pratique, le 2PC est rarement utilisé dans les architectures microservices modernes.
Le pattern Saga est l'alternative privilégiée. Au lieu d'une transaction atomique globale, il découpe l'opération en une séquence de transactions locales, chacune avec une action de compensation en cas d'échec.
Une Saga est une séquence de transactions locales où :
- Chaque transaction met à jour une base de données et publie un événement
- Si une étape échoue, des transactions de compensation sont exécutées pour annuler les étapes précédentes
- La cohérence est éventuelle (eventual consistency), pas immédiate
Flux Normal (Happy Path)
═══════════════════════════════════════════════════════════════►
T1 T2 T3 T4
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Créer│───────►│Réser│───────►│Débi-│───────►│Plani│
│Comm.│ │Stock│ │ ter │ │Livr.│
└─────┘ └─────┘ └─────┘ └─────┘
Flux avec Échec et Compensation
═══════════════════════════════════════════════════════════════►
T1 T2 T3 T4
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Créer│───────►│Réser│───────►│Débi-│───────►│Plani│ ✗ ÉCHEC
│Comm.│ │Stock│ │ ter │ │Livr.│
└─────┘ └─────┘ └─────┘ └─────┘
│
◄═══════════════════════════════╧═══════════════════════════════
COMPENSATIONS
C3 C2 C1
┌─────┐ ┌─────┐ ┌─────┐
│Rem- │◄───────│Libé-│◄───────│Annu-│
│bours│ │Stock│ │Comm.│
└─────┘ └─────┘ └─────┘
Il existe deux façons d'implémenter une Saga : Orchestration et Chorégraphie.
Un orchestrateur central (le "chef d'orchestre") coordonne toutes les étapes de la Saga. Il sait quelles étapes exécuter, dans quel ordre, et quelles compensations déclencher en cas d'échec.
┌──────────────────┐
│ Orchestrateur │
│ (OrderSaga) │
└────────┬─────────┘
│
┌───────────┬───────────┼───────────┬───────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Service │ │ Service │ │ Service │ │ Service │ │ Service │
│Commandes│ │ Stock │ │Paiement │ │Livraison│ │ Notif. │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
L'orchestrateur envoie des commandes aux services et attend leurs réponses pour décider de la suite.
Voici comment modéliser une Saga de commande avec PostgreSQL comme stockage de l'état.
-- Table pour suivre l'état de chaque saga
CREATE TABLE order_sagas (
saga_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL,
current_step VARCHAR(50) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'STARTED',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Données de contexte
user_id INTEGER NOT NULL,
total_amount NUMERIC(10,2) NOT NULL,
items JSONB NOT NULL,
-- Résultats des étapes
payment_id UUID,
shipment_id UUID,
-- Contrainte sur le statut
CONSTRAINT valid_status CHECK (
status IN ('STARTED', 'PENDING', 'COMPLETED', 'COMPENSATING', 'FAILED')
)
);
-- Table pour l'historique des étapes
CREATE TABLE saga_steps (
id SERIAL PRIMARY KEY,
saga_id UUID REFERENCES order_sagas(saga_id),
step_name VARCHAR(50) NOT NULL,
step_type VARCHAR(20) NOT NULL, -- 'FORWARD' ou 'COMPENSATE'
status VARCHAR(20) NOT NULL,
executed_at TIMESTAMPTZ DEFAULT NOW(),
error_message TEXT,
CONSTRAINT valid_step_status CHECK (
status IN ('PENDING', 'SUCCESS', 'FAILED')
)
);-- Table de définition des étapes de la saga
CREATE TABLE saga_step_definitions (
step_order INTEGER PRIMARY KEY,
step_name VARCHAR(50) NOT NULL UNIQUE,
service_name VARCHAR(50) NOT NULL,
command_type VARCHAR(50) NOT NULL,
compensation_type VARCHAR(50),
is_compensatable BOOLEAN DEFAULT TRUE
);
-- Insertion des étapes pour la saga de commande
INSERT INTO saga_step_definitions VALUES
(1, 'CREATE_ORDER', 'order-service', 'CreateOrder', 'CancelOrder', TRUE),
(2, 'RESERVE_STOCK', 'stock-service', 'ReserveStock', 'ReleaseStock', TRUE),
(3, 'PROCESS_PAYMENT', 'payment-service', 'ProcessPayment', 'RefundPayment', TRUE),
(4, 'SCHEDULE_SHIPMENT','shipping-service', 'ScheduleShip', 'CancelShipment', TRUE),
(5, 'SEND_CONFIRMATION','notif-service', 'SendEmail', NULL, FALSE);-- Fonction pour avancer la saga à l'étape suivante
CREATE OR REPLACE FUNCTION advance_saga(p_saga_id UUID, p_step_result VARCHAR, p_error TEXT DEFAULT NULL)
RETURNS void AS $$
DECLARE
v_saga RECORD;
v_current_step RECORD;
v_next_step RECORD;
BEGIN
-- Récupérer l'état actuel de la saga
SELECT * INTO v_saga FROM order_sagas WHERE saga_id = p_saga_id FOR UPDATE;
-- Récupérer la définition de l'étape actuelle
SELECT * INTO v_current_step
FROM saga_step_definitions
WHERE step_name = v_saga.current_step;
IF p_step_result = 'SUCCESS' THEN
-- Enregistrer le succès
INSERT INTO saga_steps (saga_id, step_name, step_type, status)
VALUES (p_saga_id, v_saga.current_step, 'FORWARD', 'SUCCESS');
-- Trouver l'étape suivante
SELECT * INTO v_next_step
FROM saga_step_definitions
WHERE step_order = v_current_step.step_order + 1;
IF v_next_step IS NULL THEN
-- Saga terminée avec succès
UPDATE order_sagas
SET status = 'COMPLETED', updated_at = NOW()
WHERE saga_id = p_saga_id;
ELSE
-- Passer à l'étape suivante
UPDATE order_sagas
SET current_step = v_next_step.step_name,
status = 'PENDING',
updated_at = NOW()
WHERE saga_id = p_saga_id;
-- Ici, déclencher l'appel au service suivant
-- (via message queue, API call, etc.)
END IF;
ELSIF p_step_result = 'FAILED' THEN
-- Enregistrer l'échec
INSERT INTO saga_steps (saga_id, step_name, step_type, status, error_message)
VALUES (p_saga_id, v_saga.current_step, 'FORWARD', 'FAILED', p_error);
-- Démarrer la compensation
UPDATE order_sagas
SET status = 'COMPENSATING', updated_at = NOW()
WHERE saga_id = p_saga_id;
-- Lancer les compensations (voir fonction suivante)
PERFORM start_compensation(p_saga_id);
END IF;
END;
$$ LANGUAGE plpgsql;| Avantage | Description |
|---|---|
| Logique centralisée | Toute la logique est dans l'orchestrateur, facile à comprendre |
| Flux explicite | L'ordre des étapes est clairement défini |
| Débogage simplifié | Un seul endroit pour suivre l'état de la saga |
| Gestion des erreurs | L'orchestrateur contrôle les compensations |
| Inconvénient | Description |
|---|---|
| Point central | L'orchestrateur peut devenir un goulot d'étranglement |
| Couplage | L'orchestrateur connaît tous les services |
| Complexité croissante | Peut devenir difficile à maintenir avec beaucoup d'étapes |
Dans la chorégraphie, il n'y a pas de coordinateur central. Chaque service écoute les événements des autres services et réagit en conséquence. Les services "dansent" ensemble sans chef d'orchestre.
┌─────────────────────────────────────────────────────────────────┐
│ Message Broker │
│ (ex: RabbitMQ, Kafka) │
└───────┬─────────┬─────────┬─────────┬─────────┬─────────────────┘
│ │ │ │ │
Publie Écoute Publie Écoute Publie
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Service │ │ Service │ │ Service │ │ Service │
│Commandes│ │ Stock │ │Paiement │ │Livraison│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Événements :
OrderCreated ──► StockReserved ──► PaymentProcessed ──► ShipmentScheduled
1. Client passe commande
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Commandes │
│ - Crée la commande (status: PENDING) │
│ - Publie: OrderCreated { orderId, items, userId, amount } │
└─────────────────────────────────────────────────────────────────┘
│
│ OrderCreated
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Stock │
│ - Écoute: OrderCreated │
│ - Réserve le stock │
│ - Publie: StockReserved { orderId, reservationId } │
│ OU │
│ - Publie: StockInsufficient { orderId, reason } │
└─────────────────────────────────────────────────────────────────┘
│
│ StockReserved
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Paiement │
│ - Écoute: StockReserved │
│ - Traite le paiement │
│ - Publie: PaymentSucceeded { orderId, paymentId } │
│ OU │
│ - Publie: PaymentFailed { orderId, reason } │
└─────────────────────────────────────────────────────────────────┘
│
│ PaymentSucceeded
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Livraison │
│ - Écoute: PaymentSucceeded │
│ - Planifie la livraison │
│ - Publie: ShipmentScheduled { orderId, shipmentId, eta } │
└─────────────────────────────────────────────────────────────────┘
Quand une étape échoue, elle publie un événement d'échec. Les services précédents écoutent cet événement et exécutent leur compensation.
PaymentFailed publié
│
├──────────────────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Service Stock │ │ Service Commandes │
│ Écoute: │ │ Écoute: │
│ PaymentFailed │ │ PaymentFailed │
│ Action: │ │ Action: │
│ Libère le stock │ │ Annule commande │
│ Publie: │ │ Publie: │
│ StockReleased │ │ OrderCancelled │
└───────────────────┘ └───────────────────┘
PostgreSQL offre un mécanisme natif de publication/souscription avec LISTEN et NOTIFY. Bien qu'il ne remplace pas un vrai message broker pour la production, il est utile pour comprendre le concept.
-- Service Commandes : Publication d'événement
CREATE OR REPLACE FUNCTION notify_order_created()
RETURNS TRIGGER AS $$
BEGIN
-- Publier l'événement sur le canal 'order_events'
PERFORM pg_notify(
'order_events',
json_build_object(
'event_type', 'OrderCreated',
'order_id', NEW.id,
'user_id', NEW.user_id,
'total', NEW.total,
'items', NEW.items,
'timestamp', NOW()
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_created_trigger
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_order_created(); -- Service Stock : Écoute et réaction (conceptuel)
-- En pratique, cela serait dans le code applicatif
-- Côté applicatif (pseudocode Python avec psycopg)
--
-- conn.execute("LISTEN order_events")
-- while True:
-- conn.poll()
-- for notify in conn.notifies:
-- event = json.loads(notify.payload)
-- if event['event_type'] == 'OrderCreated':
-- reserve_stock(event['order_id'], event['items'])| Avantage | Description |
|---|---|
| Découplage fort | Les services ne se connaissent pas directement |
| Pas de point central | Pas de goulot d'étranglement |
| Scalabilité | Chaque service évolue indépendamment |
| Résilience | La panne d'un service n'arrête pas les autres |
| Inconvénient | Description |
|---|---|
| Flux difficile à suivre | La logique est dispersée dans tous les services |
| Débogage complexe | Nécessite une bonne observabilité (tracing distribué) |
| Dépendances cycliques | Risque de créer des boucles d'événements |
| Cohérence difficile | Plus complexe de garantir que toutes les compensations s'exécutent |
Un problème critique des Sagas est la publication fiable des événements. Que se passe-t-il si le service met à jour sa base de données mais plante avant de publier l'événement ?
1. BEGIN transaction
2. UPDATE orders SET status = 'confirmed' ✓ Succès
3. COMMIT ✓ Succès
4. Publier événement OrderConfirmed ✗ CRASH !
Résultat : Base mise à jour, mais événement jamais publié.
Les autres services ne sont pas informés.
Au lieu de publier directement, on écrit l'événement dans une table Outbox dans la même transaction que la modification métier.
-- Table Outbox pour les événements à publier
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(100) NOT NULL, -- ex: 'Order'
aggregate_id UUID NOT NULL, -- ex: order_id
event_type VARCHAR(100) NOT NULL, -- ex: 'OrderCreated'
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
published_at TIMESTAMPTZ, -- NULL = pas encore publié
-- Index pour le polling efficace
CONSTRAINT idx_outbox_unpublished
CHECK (published_at IS NOT NULL OR TRUE)
);
CREATE INDEX idx_outbox_pending ON outbox(created_at)
WHERE published_at IS NULL; -- Transaction atomique : modification + événement
BEGIN;
-- Modification métier
INSERT INTO orders (id, user_id, total, status, items)
VALUES ('ord-123', 42, 150.00, 'pending', '["item1", "item2"]');
-- Événement dans la même transaction
INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)
VALUES (
'Order',
'ord-123',
'OrderCreated',
jsonb_build_object(
'orderId', 'ord-123',
'userId', 42,
'total', 150.00,
'items', '["item1", "item2"]'::jsonb
)
);
COMMIT;
-- Les deux écritures réussissent ou échouent ensemble !Un processus séparé (polling ou CDC) lit la table Outbox et publie les événements.
-- Récupérer les événements non publiés
SELECT * FROM outbox
WHERE published_at IS NULL
ORDER BY created_at
LIMIT 100
FOR UPDATE SKIP LOCKED; -- Évite les conflits en parallèle
-- Après publication réussie au message broker
UPDATE outbox
SET published_at = NOW()
WHERE id = 'evt-xxx'; Pour les systèmes à fort volume, Debezium peut capturer les changements de la table Outbox directement depuis le WAL de PostgreSQL, sans polling.
PostgreSQL WAL ──► Debezium ──► Kafka ──► Services consommateurs
Dans un système distribué, les messages peuvent être délivrés plusieurs fois (at-least-once delivery). Chaque étape de la Saga doit être idempotente : exécutée plusieurs fois, elle produit le même résultat.
-- ❌ NON IDEMPOTENT : Déduire le stock à chaque appel
UPDATE products SET stock = stock - 1 WHERE id = 501;
-- Problème : Si appelé 3 fois, stock déduit de 3 !
-- ✅ IDEMPOTENT : Utiliser une réservation unique
INSERT INTO stock_reservations (reservation_id, product_id, quantity)
VALUES ('res-abc', 501, 1)
ON CONFLICT (reservation_id) DO NOTHING;
-- Appelé 3 fois = 1 seule réservation créée| Stratégie | Description |
|---|---|
| Clé d'idempotence | Stocker l'ID de chaque requête traitée |
| Upsert conditionnel | ON CONFLICT DO NOTHING ou DO UPDATE |
| État final | Vérifier l'état avant modification |
| Version optimiste | Utiliser un numéro de version |
-- Table pour tracker les requêtes traitées
CREATE TABLE processed_requests (
request_id UUID PRIMARY KEY,
processed_at TIMESTAMPTZ DEFAULT NOW(),
result JSONB
);
-- Vérifier avant traitement
CREATE OR REPLACE FUNCTION process_payment_idempotent(
p_request_id UUID,
p_order_id UUID,
p_amount NUMERIC
) RETURNS JSONB AS $$
DECLARE
v_existing RECORD;
v_result JSONB;
BEGIN
-- Vérifier si déjà traité
SELECT * INTO v_existing
FROM processed_requests
WHERE request_id = p_request_id;
IF FOUND THEN
-- Retourner le résultat précédent
RETURN v_existing.result;
END IF;
-- Traiter le paiement...
-- ... logique métier ...
v_result := jsonb_build_object('status', 'success', 'payment_id', 'pay-xyz');
-- Enregistrer le traitement
INSERT INTO processed_requests (request_id, result)
VALUES (p_request_id, v_result);
RETURN v_result;
END;
$$ LANGUAGE plpgsql;Il est crucial de comprendre que les compensations ne sont pas des rollbacks classiques.
- Annule les modifications non commitées
- Automatique et complet
- Aucune trace laissée
- Géré par le SGBD
- Crée une nouvelle opération qui "annule" l'effet de la précédente
- Doit être explicitement programmée
- Laisse une trace dans l'historique
- Peut être plus complexe que l'opération originale
-- OPÉRATION ORIGINALE : Réserver du stock
INSERT INTO stock_reservations (id, product_id, quantity, status)
VALUES ('res-123', 501, 5, 'active');
UPDATE products SET available_stock = available_stock - 5
WHERE id = 501;
-- COMPENSATION (ce n'est PAS un rollback !)
-- On ne supprime pas la réservation, on la marque comme annulée
UPDATE stock_reservations
SET status = 'cancelled', cancelled_at = NOW()
WHERE id = 'res-123';
UPDATE products SET available_stock = available_stock + 5
WHERE id = 501;
-- L'historique montre : réservation créée PUIS annuléeCertaines opérations ne peuvent pas être parfaitement compensées :
| Opération | Compensation possible |
|---|---|
| Envoyer un email | ❌ Impossible d'annuler (on peut envoyer un autre email d'excuse) |
| Facturer un client | |
| Publier sur réseau social | ❌ Ou |
| Réserver un billet d'avion |
Pour ces cas, on parle de transactions de mitigation plutôt que de vraies compensations.
| Critère | Orchestration | Chorégraphie |
|---|---|---|
| Nombre d'étapes | Beaucoup (5+) | Peu (2-4) |
| Complexité logique | Élevée | Simple |
| Besoin de visibilité | Fort | Faible |
| Équipes | Une équipe centrale | Équipes autonomes |
| Évolutivité logique | Modifications centralisées | Modifications distribuées |
| Latence acceptable | Moins critique | Plus critique |
- Débutez avec l'orchestration si vous n'êtes pas familier avec les architectures événementielles
- Passez à la chorégraphie lorsque le découplage devient plus important que la visibilité
- Hybride : Orchestration pour les flux critiques, chorégraphie pour les flux secondaires
Toujours écrire les événements dans une table Outbox dans la même transaction que la modification métier.
Utilisez des clés d'idempotence et des upserts conditionnels.
Maintenez une table de suivi pour chaque saga, avec l'historique des étapes.
Les sagas peuvent rester bloquées. Prévoyez des timeouts et des mécanismes de reprise.
-- Trouver les sagas bloquées depuis plus d'une heure
SELECT * FROM order_sagas
WHERE status = 'PENDING'
AND updated_at < NOW() - INTERVAL '1 hour'; Les événements qui échouent de manière répétée doivent être mis de côté pour analyse manuelle.
- Tracing distribué (OpenTelemetry)
- Logs corrélés par saga_id
- Métriques sur les durées et taux d'échec
Voici quelques outils qui facilitent l'implémentation des Sagas :
| Outil | Description |
|---|---|
| Temporal.io | Orchestration de workflows durable et résiliente |
| Eventuate Tram | Framework Saga pour Java avec support Outbox |
| MassTransit | Framework .NET avec saga state machine |
| NServiceBus | Saga pattern pour .NET avec persistance PostgreSQL |
| Debezium | CDC pour publier les événements Outbox |
| Apache Kafka | Message broker pour la chorégraphie |
Les transactions distribuées sont un défi inévitable dans les architectures microservices. Plutôt que de forcer des transactions ACID globales (2PC), le pattern Saga offre une approche pragmatique :
- Découper en transactions locales indépendantes
- Prévoir des compensations pour chaque étape
- Accepter la cohérence éventuelle
- Garantir l'idempotence de chaque opération
PostgreSQL, grâce à ses transactions locales robustes, son support JSONB pour les événements, et ses mécanismes comme LISTEN/NOTIFY, est un excellent choix pour implémenter les Sagas.
Le choix entre orchestration et chorégraphie dépend de votre contexte : complexité du flux, besoin de visibilité, et organisation des équipes.
- 2PC : Protocole classique mais peu adapté aux microservices (verrouillage, latence, couplage)
- Saga : Séquence de transactions locales avec compensations en cas d'échec
- Orchestration : Un coordinateur central dirige le flux (visibilité, mais point central)
- Chorégraphie : Les services réagissent aux événements (découplage, mais complexité)
- Pattern Outbox : Garantit la publication fiable des événements
- Idempotence : Chaque étape doit pouvoir être rejouée sans effet de bord
- Compensation ≠ Rollback : C'est une nouvelle opération qui annule l'effet, pas un retour en arrière