Développement Web

Saga pattern : transactions distribuées orchestration et chorégraphie

14 min de lecture
📍 Vue d’ensemble : ce tutoriel s’inscrit dans une série sur l’architecture logicielle. Pour la carte conceptuelle, lisez le guide principal Architecture logicielle moderne : DDD, microservices et event-driven.

Confirmer une commande, dans une boutique répartie en services, implique trois actions : réserver le stock, encaisser le paiement, planifier la livraison. Dans un monolithe, une transaction de base de données garantit que tout réussit ou que tout est annulé. Mais quand ces trois opérations vivent dans trois services aux bases distinctes, cette garantie disparaît. Le pattern Saga, dont l’idée fondatrice remonte à un article de Hector Garcia-Molina et Kenneth Salem publié en 1987, résout ce problème par une suite de transactions locales et des compensations. Dans ce tutoriel, vous allez implémenter cette coordination en Java avec Spring Boot, selon ses deux styles : la chorégraphie et l’orchestration.

🎯 Ce que vous allez apprendre

  • Comprendre pourquoi une transaction ACID classique est impossible entre services.
  • Décomposer une opération distribuée en transactions locales et compensations.
  • Implémenter une saga par chorégraphie (les services réagissent aux événements).
  • Implémenter une saga par orchestration (un coordinateur central pilote la séquence).
  • Rendre chaque étape idempotente et choisir le style adapté à votre cas.

🛠️ Ce que vous allez construire

Un coordinateur de commande qui enchaîne réservation de stock, paiement et livraison, et qui sait défaire proprement les étapes déjà accomplies si l’une échoue. Vous verrez le même processus exprimé d’abord en chorégraphie, puis en orchestration, et vous saurez à la fin lequel choisir selon le contexte.

Prérequis

  • Java 21+ et Spring Boot 4 (paru en novembre 2025).
  • Idéalement, avoir vu l’architecture orientée événements, car la chorégraphie repose sur la messagerie.
  • Niveau intermédiaire à avancé. Test express : si vous comprenez ce qu’est une transaction de base de données et un appel entre services, vous êtes prêt.
  • ⏱️ Temps estimé : ~65 minutes.

Étape 1 — Pourquoi pas une simple transaction ?

On pourrait être tenté d’envelopper les trois opérations dans une grande transaction distribuée, à l’aide d’un protocole comme le two-phase commit. C’est techniquement possible, mais déconseillé dans une architecture de services : ce protocole verrouille les ressources de tous les participants pendant toute la durée de l’opération, ce qui effondre la disponibilité et la performance dès que la charge monte. Un service lent bloque alors tous les autres. C’est exactement le couplage que les microservices cherchent à fuir.

La saga accepte donc un compromis assumé : renoncer à l’atomicité immédiate au profit d’une cohérence à terme. Plutôt qu’un « tout ou rien » instantané, on exécute une suite d’étapes, chacune validée localement dans son propre service. Si une étape échoue, on ne fait pas marche arrière par un rollback technique — impossible, puisque les étapes précédentes ont déjà été validées dans d’autres bases — mais par des transactions de compensation qui annulent sémantiquement ce qui a été fait : rembourser un paiement, libérer un stock réservé.

Une grille de lecture, proposée par Chris Richardson, aide à raisonner sur chaque étape. Une transaction compensable peut être annulée par une compensation : réserver du stock, par exemple. Une transaction pivot est le point de non-retour : une fois franchie, la saga ira jusqu’au bout — souvent le paiement. Une transaction réessayable vient après le pivot et finira par réussir si on la relance : planifier la livraison entre dans cette catégorie. Ordonner ses étapes selon cette classification — d’abord les compensables, puis le pivot, enfin les réessayables — simplifie énormément la conception : on sait exactement jusqu’où on peut reculer et à partir d’où on ne peut qu’avancer.

Point d’étape — Retenez la nuance entre rollback et compensation. Un rollback efface une opération comme si elle n’avait jamais eu lieu. Une compensation, elle, ajoute une nouvelle opération qui en annule les effets. Le remboursement laisse une trace ; il ne fait pas disparaître le paiement initial de l’historique.

Étape 2 — La chorégraphie : les services dansent ensemble

Dans le style chorégraphié, aucun chef d’orchestre. Chaque service réagit à un événement, fait son travail, puis publie un nouvel événement qui déclenche le suivant. La logique de la saga est répartie entre les participants. Le service de commandes publie OrderConfirmed ; le service de stock l’écoute, réserve, et publie StockReserved ; le service de paiement écoute ce dernier, encaisse, et publie PaymentCharged ; et ainsi de suite.

// InventoryService.java — un participant de la saga chorégraphiée
@Component
public class InventoryService {

    private final EventPublisher publisher;

    public InventoryService(EventPublisher publisher) {
        this.publisher = publisher;
    }

    @EventListener
    public void on(OrderConfirmed event) {
        boolean reserved = tryReserve(event.orderId(), event.items());
        if (reserved) {
            publisher.publish(new StockReserved(event.orderId()));
        } else {
            // déclenche la compensation en amont
            publisher.publish(new StockReservationFailed(event.orderId()));
        }
    }

    @EventListener
    public void on(PaymentFailed event) {
        // compensation : on libère le stock réservé précédemment
        release(event.orderId());
    }
}

Ici, @EventListener de Spring relie chaque méthode à un type d’événement ; en production, ces événements transiteraient par une messagerie comme RabbitMQ entre services séparés, pas en mémoire. L’élégance de la chorégraphie est l’absence de point central : on ajoute un participant en l’abonnant simplement aux bons événements. Sa faiblesse est la lisibilité : le déroulé complet de la saga n’est écrit nulle part en un seul endroit. Pour comprendre le scénario, il faut suivre les événements de service en service, ce qui devient vite difficile au-delà de trois ou quatre étapes.

Étape 3 — L’orchestration : un chef d’orchestre explicite

Le style orchestré confie la séquence à un composant unique, l’orchestrateur, qui appelle chaque service dans l’ordre et décide des compensations. La logique de la saga devient lisible d’un coup d’œil, au prix d’un composant qui connaît tous les participants. C’est souvent le bon choix dès que le processus comporte plusieurs branches ou conditions.

// OrderSagaOrchestrator.java
@Service
public class OrderSagaOrchestrator {

    private final InventoryClient inventory;
    private final PaymentClient payment;
    private final DeliveryClient delivery;

    public OrderSagaOrchestrator(InventoryClient inventory,
                                 PaymentClient payment,
                                 DeliveryClient delivery) {
        this.inventory = inventory;
        this.payment = payment;
        this.delivery = delivery;
    }

    public SagaResult process(OrderContext ctx) {
        // Étape 1 : réserver le stock
        if (!inventory.reserve(ctx.orderId(), ctx.items())) {
            return SagaResult.failed("Stock insuffisant");
        }

        // Étape 2 : encaisser le paiement
        try {
            payment.charge(ctx.orderId(), ctx.amountCents());
        } catch (PaymentException e) {
            inventory.release(ctx.orderId(), ctx.items());     // compensation étape 1
            return SagaResult.failed("Paiement refusé");
        }

        // Étape 3 : planifier la livraison
        try {
            delivery.schedule(ctx.orderId(), ctx.address());
        } catch (DeliveryException e) {
            payment.refund(ctx.orderId(), ctx.amountCents());  // compensations en ordre inverse
            inventory.release(ctx.orderId(), ctx.items());
            return SagaResult.failed("Livraison indisponible");
        }

        return SagaResult.completed();
    }
}

Le contexte et le résultat se modélisent élégamment avec des records Java, qui conviennent parfaitement à ces porteurs de données immuables.

// types de support
public record OrderContext(String orderId, List<Item> items, long amountCents, String address) {}

public record SagaResult(boolean success, String reason) {
    public static SagaResult completed() { return new SagaResult(true, null); }
    public static SagaResult failed(String reason) { return new SagaResult(false, reason); }
}

Tout le scénario se lit ici, du haut vers le bas, y compris les compensations. Le détail crucial est l’ordre inverse des compensations : si la livraison échoue, on rembourse d’abord le paiement, puis on libère le stock — on défait les étapes dans l’ordre exactement opposé à celui où on les a faites. Cette discipline évite les états incohérents pendant la remontée. L’orchestrateur sacrifie l’autonomie totale des participants, mais gagne une clarté qui vaut de l’or lors du débogage d’un processus métier complexe.

Étape 4 — L’idempotence, condition de survie

Une saga distribuée vit dans un monde où les messages se perdent, se dupliquent et arrivent en désordre. Un appel à charge peut échouer par expiration côté orchestrateur alors que le paiement a bel et bien été encaissé côté service. Si l’orchestrateur réessaie naïvement, le client serait débité deux fois. Chaque étape et chaque compensation doivent donc être idempotentes : les exécuter plusieurs fois doit produire le même effet qu’une seule.

// PaymentService.java — un encaissement idempotent
@Service
public class PaymentService {

    private final ChargeRepository charges;

    public PaymentService(ChargeRepository charges) {
        this.charges = charges;
    }

    @Transactional
    public void charge(String orderId, long amountCents) {
        // si une opération existe déjà pour cette commande, on ne refait rien
        if (charges.existsByOrderId(orderId)) {
            return;
        }
        charges.save(new Charge(orderId, amountCents));
        // ... appel réel au prestataire de paiement ...
    }
}

La clé est ici l’identifiant de la commande, utilisé comme clé d’idempotence : avant d’encaisser, on vérifie qu’aucune opération n’existe déjà pour cette commande. Une contrainte d’unicité en base verrouille définitivement la garantie, même en cas d’appels concurrents. Sans cette précaution, aucune saga n’est sûre en production : les reprises sur erreur, indispensables, deviendraient elles-mêmes une source de corruption.

Point d’étape — Pour chaque étape de votre saga, posez-vous la question : « si je l’exécute deux fois, qu’arrive-t-il ? ». Si la réponse n’est pas « rien de plus », ajoutez une clé d’idempotence avant d’aller plus loin. C’est non négociable.

Étape 5 — Vérifier le chemin d’échec

Le chemin nominal — tout réussit — se teste facilement. Le vrai enjeu est le chemin d’échec : prouver que les compensations ramènent le système dans un état cohérent. On simule une panne de paiement et l’on vérifie que le stock réservé est bien libéré.

// OrderSagaOrchestratorTest.java (extrait JUnit 5 + Mockito)
@Test
void libere_le_stock_si_le_paiement_echoue() {
    when(inventory.reserve(anyString(), anyList())).thenReturn(true);
    doThrow(new PaymentException("refusé"))
        .when(payment).charge(anyString(), anyLong());

    SagaResult result = orchestrator.process(sampleOrder());

    assertThat(result.success()).isFalse();
    verify(inventory).release(eq("ord-1"), anyList()); // compensation déclenchée
    verify(delivery, never()).schedule(anyString(), anyString()); // jamais atteint
}

Ce test capture l’essentiel d’une saga : non pas que le scénario heureux fonctionne, mais que l’échec d’une étape déclenche les bonnes compensations et n’atteint jamais les étapes suivantes. On vérifie que release a été appelé et que schedule ne l’a jamais été. C’est ce genre de test qui donne confiance pour mettre une transaction distribuée en production : on a prouvé que le système sait se rattraper.

Étape 6 — Chorégraphie ou orchestration ?

Aucun des deux styles n’est universellement meilleur ; ils répondent à des contraintes différentes. La chorégraphie excelle pour les processus simples et linéaires, où l’autonomie des services prime et où l’on veut éviter tout point central. L’orchestration s’impose dès que le processus se complique : branches conditionnelles, étapes optionnelles, besoin de visualiser et de surveiller le déroulé. Beaucoup d’équipes commencent en chorégraphie et basculent vers l’orchestration quand la saga devient difficile à suivre.

Un critère pratique tranche souvent : demandez-vous si quelqu’un, dans six mois, devra comprendre le processus complet pour le modifier ou le déboguer. Si oui, l’orchestration, qui rassemble la logique en un endroit, lui rendra un immense service. Si le processus est trivial et stable, la chorégraphie reste plus légère. Des outils dédiés existent pour l’orchestration de sagas à grande échelle, mais le principe que vous venez d’implémenter à la main en est toujours le cœur.

Observer une saga en production

Une transaction de base de données est invisible quand tout va bien : elle réussit, point. Une saga, elle, doit être observable, car son déroulé s’étale dans le temps et entre plusieurs services. Sans visibilité, un échec de compensation passe inaperçu jusqu’à ce qu’un client se plaigne d’un débit sans livraison. Le minimum vital consiste à attribuer un identifiant de corrélation à chaque saga — typiquement l’identifiant de commande — et à le propager dans chaque appel et chaque événement, pour pouvoir reconstituer le fil complet dans les journaux.

Au-delà des journaux, on suit deux indicateurs simples mais parlants : le taux de sagas qui se terminent en compensation (un pic signale un service défaillant en aval) et la durée moyenne d’exécution (une dérive révèle une lenteur qui rapproche du délai d’expiration). Quand une compensation échoue de façon répétée, une alerte doit réveiller quelqu’un : contrairement à un simple appel raté, une compensation en échec laisse de l’argent ou du stock dans un état incohérent. Concevoir la saga, c’est donc aussi concevoir sa surveillance — les deux ne se séparent pas.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
Client débité deux fois Étape de paiement non idempotente + reprise Clé d’idempotence (identifiant de commande) + contrainte d’unicité
Stock jamais libéré après échec Compensation oubliée ou non déclenchée Tester explicitement le chemin d’échec, compenser en ordre inverse
Saga chorégraphiée illisible Logique éparpillée sur trop de services Passer à l’orchestration au-delà de 3-4 étapes
Compensations dans le mauvais ordre On défait dans l’ordre direct Toujours compenser dans l’ordre inverse des étapes
Verrous et lenteurs généralisés Tentative de transaction distribuée (2PC) Préférer la saga : transactions locales + compensations

✅ Récapitulatif

Vous savez désormais coordonner une opération qui franchit plusieurs services sans verrou distribué. La saga remplace l’atomicité immédiate par une suite de transactions locales et de compensations sémantiques. Vous l’avez exprimée en chorégraphie, où les services réagissent à des événements, puis en orchestration, où un coordinateur lisible pilote la séquence et défait les étapes en ordre inverse en cas d’échec. Et vous avez ancré la règle sans laquelle rien ne tient en production : chaque étape doit être idempotente. C’est la dernière pièce qui rend une architecture de services réellement fiable.

🧾 Aide-mémoire

Notion Rôle
Transaction locale Étape validée dans la base d’un seul service
Compensation Opération qui annule sémantiquement une étape (remboursement, libération)
Chorégraphie Pas de chef : les services réagissent aux événements
Orchestration Un coordinateur central appelle les services et compense
Ordre inverse On compense les étapes dans l’ordre opposé à leur exécution
Clé d’idempotence Évite les doubles effets lors des reprises

💪 À vous de jouer

Ajoutez une quatrième étape à l’orchestrateur : l’attribution de points de fidélité après la livraison. Prévoyez sa compensation (retrait des points) au cas où une étape ultérieure échouerait, et placez-la au bon endroit dans la cascade de compensations.

Voir une solution
try {
    loyalty.award(ctx.orderId(), ctx.amountCents());
} catch (LoyaltyException e) {
    // si l'attribution échoue, on compense tout ce qui précède, en ordre inverse
    delivery.cancel(ctx.orderId());
    payment.refund(ctx.orderId(), ctx.amountCents());
    inventory.release(ctx.orderId(), ctx.items());
    return SagaResult.failed("Fidélité indisponible");
}

Si une étape encore plus tardive échouait, il faudrait ajouter loyalty.revoke(...) en tête de sa cascade de compensation.

Tutoriels liés

Pour aller plus loin

Questions fréquentes

Une saga garantit-elle la cohérence comme une transaction ?
Non, pas au même sens. Une transaction ACID offre une cohérence immédiate ; une saga offre une cohérence à terme. Entre le début et la fin de la saga, le système traverse des états intermédiaires visibles. On conçoit donc l’interface pour que ces états transitoires soient acceptables ou invisibles pour l’utilisateur.

Que faire si une compensation échoue elle aussi ?
C’est le cas le plus délicat. On rend les compensations idempotentes et on les réessaie ; si l’échec persiste, on bascule sur une intervention manuelle via une alerte. Une compensation ne doit jamais être abandonnée silencieusement : elle représente de l’argent ou du stock réel.

Faut-il une base de données pour l’orchestrateur ?
Dès que les sagas durent dans le temps ou survivent à un redémarrage, oui : on persiste l’état d’avancement de chaque saga pour pouvoir la reprendre là où elle en était. Pour des sagas courtes et synchrones comme notre exemple, ce n’est pas indispensable.

Mots-clés : pattern saga, transactions distribuées, orchestration, chorégraphie, compensation, idempotence, microservices, Spring Boot.

Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité