Digital solutions
Design Patterns avec Laravel

Saga Pattern

  • Type : Architectural
  • Difficulté : 8/10
  • Définition succincte : Le Saga Pattern est un pattern architectural qui permet de gérer des transactions distribuées complexes. Il divise une transaction longue en plusieurs sous-transactions qui sont autonomes et peuvent être compensées en cas d'échec. Il existe deux types principaux de Saga :
    • Orchestration : Une entité centrale (l'orchestrateur) gère et contrôle les étapes de la saga.
    • Chorégraphie : Chaque service réagit de manière autonome à des événements spécifiques, et la saga est orchestrée par une série de communications asynchrones entre services, sans orchestrateur central.
  • Contexte spécifique à Laravel : Bien que Laravel n'ait pas d'implémentation native du Saga Pattern, il est possible de mettre en œuvre ce pattern pour des systèmes distribués ou des architectures microservices. Par exemple, pour gérer des processus complexes de commande, d'inventaire, ou de facturation qui nécessitent une coordination entre plusieurs services.

Objectif du Saga Pattern

L'objectif du Saga Pattern est de garantir la cohérence des données à travers plusieurs services dans un système distribué, où une transaction unique ne peut pas couvrir tous les services. Il permet d'annuler ou compenser des sous-transactions si une étape échoue, tout en offrant une approche modulaire pour gérer les transactions complexes dans des environnements distribués.

Types de Saga

  1. Orchestration : Un orchestrateur central est responsable de diriger les étapes de la saga. Il décide quand chaque service doit exécuter son action et, en cas d'échec, lance les actions compensatoires.
  2. Chorégraphie : Chaque service réagit aux événements en chaîne sans intervention d'un orchestrateur central. Ici, les services communiquent par des événements asynchrones pour accomplir chaque étape de la saga.

1. Implémentation de la Saga avec Orchestration

Contexte

Imaginons un système de réservation de voyage (vol, hôtel, voiture), comme dans l'exemple précédent. Ce système sera orchestré par un orchestrateur central qui gère l'ordre dans lequel les sous-transactions sont exécutées et appelle les actions compensatoires si une étape échoue.

a) Définir la classe SagaOrchestrator

L'orchestrateur est responsable de l'exécution des étapes de la saga et de la gestion des échecs en cas de problème.

// app/Saga/SagaOrchestrator.php

namespace App\Saga;

class SagaOrchestrator
{
    protected $steps = [];

    public function addStep(SagaStep $step): void
    {
        $this->steps[] = $step;
    }

    public function execute(): void
    {
        foreach ($this->steps as $step) {
            if (!$step->execute()) {
                echo "Échec dans la saga, début de la compensation...\n";
                $this->compensate();
                return;
            }
        }
        echo "Saga complétée avec succès !\n";
    }

    protected function compensate(): void
    {
        foreach (array_reverse($this->steps) as $step) {
            $step->compensate();
        }
    }
}

b) Créer les étapes de la saga (BookFlightStep, BookHotelStep, BookCarRentalStep)

Chaque étape est responsable de sa propre exécution et de sa compensation.

// app/Saga/Steps/BookFlightStep.php
namespace App\Saga\Steps;

use App\Saga\SagaStep;

class BookFlightStep extends SagaStep
{
    public function execute(): bool
    {
        echo "Réservation du vol...\n";
        return true;
    }

    public function compensate(): void
    {
        echo "Annulation du vol...\n";
    }
}

On fait de même pour BookHotelStep et BookCarRentalStep, comme dans l'exemple précédent.

c) Utilisation du Saga avec Orchestration

La commande exécute la saga avec l'orchestrateur central.

// app/Console/Commands/BookTrip.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Saga\SagaOrchestrator;
use App\Saga\Steps\BookFlightStep;
use App\Saga\Steps\BookHotelStep;
use App\Saga\Steps\BookCarRentalStep;

class BookTrip extends Command
{
    protected $signature = 'trip:book';
    protected $description = 'Réservation de voyage avec Saga Pattern (Orchestration)';

    public function handle()
    {
        $saga = new SagaOrchestrator();
        $saga->addStep(new BookFlightStep());
        $saga->addStep(new BookHotelStep());
        $saga->addStep(new BookCarRentalStep());
        $saga->execute();
    }
}

En exécutant cette commande, tu peux voir que l'orchestrateur gère le processus et lance les actions compensatoires en cas d'échec.

2. Implémentation de la Saga avec Chorégraphie

Contexte

Dans cette approche, chaque service communique via des événements. Il n'y a pas d'orchestrateur central. Par exemple, lorsqu'une réservation de vol réussit, un événement est émis, et le service de réservation d'hôtel réagit en réservant l'hôtel.

a) Créer les services événementiels

Dans cette approche, chaque service écoute des événements et déclenche ses actions en fonction de ces événements.

1. Service FlightBookedEvent

Lorsqu'un vol est réservé, cet événement est émis pour informer les autres services.

// app/Events/FlightBookedEvent.php

namespace App\Events;

class FlightBookedEvent
{
    public $flightDetails;

    public function __construct($flightDetails)
    {
        $this->flightDetails = $flightDetails;
    }
}
2. Écouter l'événement de réservation d'hôtel

Lorsqu'un hôtel est réservé, cet événement est émis pour informer les autres services.

// app/Events/HotelBookedEvent.php

namespace App\Events;

class HotelBookedEvent
{
    public $hotelDetails;

    public function __construct($hotelDetails)
    {
        $this->hotelDetails = $hotelDetails;
    }
}

Le service de réservation d'hôtel écoute l'événement de réservation de vol et réagit en réservant l'hôtel.

// app/Listeners/BookHotel.php

namespace App\Listeners;

use App\Events\FlightBookedEvent;
use App\Events\HotelBookedEvent;

class BookHotel
{
    public function handle(FlightBookedEvent $event)
    {
        echo "Réservation de l'hôtel pour le vol : " . $event->flightDetails . "\n";
        event(new HotelBookedEvent('Détails de l\'hôtel'));
    }
}
3. Service de réservation de voiture

Lorsque la voiture est reservée, cet événement est émis pour informer les autres services.

// app/Events/CarBookedEvent.php

namespace App\Events;

class CarBookedEvent
{
    public $carDetails;

    public function __construct($carDetails)
    {
        $this->carDetails = $carDetails;
    }
}

Le service de reservation peut écouter l'événement HotelBookedEvent pour déclencher la réservation de voiture.

// app/Listeners/BookCar.php

namespace App\Listeners;

use App\Events\HotelBookedEvent;
use App\Events\CarBookedEvent;

class BookCar
{
    public function handle(HotelBookedEvent $event)
    {
        echo "Réservation de la voiture pour l'hôtel : " . $event->hotelDetails . "\n";
        event(new CarBookedEvent('Détails de la voiture'));
    }
}

b) Émission des événements dans une commande CLI

Chaque étape émet un événement, et le service suivant réagit.

// app/Console/Commands/TripChoreography.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Events\FlightBookedEvent;

class TripChoreography extends Command
{
    protected $signature = 'trip:choreography';
    protected $description = 'Réservation de voyage avec Saga Pattern (Chorégraphie)';

    public function handle()
    {
        // Simuler la réservation d'un vol et émettre un événement
        echo "Réservation du vol...\n";
        event(new FlightBookedEvent('Détails du vol'));

        // Ensuite, les services BookHotel et BookCar réagiront aux événements émis
    }
}

c) Exécution de la commande

En exécutant la commande suivante, les services réagiront automatiquement aux événements déclenchés :

php artisan trip:choreography

Résultat attendu :

Réservation du vol...
Réservation de l'hôtel pour le vol : Détails du vol
Réservation de la voiture pour l'hôtel : Détails de l'hôtel

Comparaison des deux approches

  • Orchestration : Une entité centrale gère toutes les étapes et prend les décisions concernant l'ordre d'exécution et la compensation. Cela offre plus de contrôle et de visibilité, mais peut devenir un goulot d'étranglement.
  • Chorégraphie : Les services communiquent entre eux de manière asynchrone via des événements. Cette approche est plus décentralisée et scalable, mais elle peut devenir difficile à suivre lorsque les services sont nombreux et que la communication devient complexe.

Avantages du Saga Pattern

  1. Cohérence garantie dans les systèmes distribués : Le pattern assure que les sous-transactions distribuées sont cohérentes, avec des actions compensatoires en cas d'échec.
  2. Scalabilité : Le pattern est adapté aux architectures distribuées et microservices.
  3. Flexibilité : Les deux approches (orchestration et chorégraphie) permettent de s'adapter à différents scénarios en fonction de la complexité des interactions entre les services.

Inconvénients du Saga Pattern

  1. Complexité accrue : Le Saga Pattern introduit une couche supplémentaire de complexité, en particulier dans la gestion des actions compensatoires.
  2. Chorégraphie difficile à suivre : Dans une approche basée sur la chorégraphie, la multiplication des événements peut rendre difficile la compréhension du flux complet de la transaction.

Conclusion

Le Saga Pattern est un excellent choix pour gérer des transactions distribuées complexes dans des architectures basées sur des microservices. Selon le besoin, tu peux choisir entre une orchestration centralisée, qui offre plus de contrôle, ou une chorégraphie décentralisée, qui favorise la scalabilité et la flexibilité. En Laravel, ce pattern peut être implémenté via les événements pour simuler une saga basée sur la chorégraphie, ou via des orchestrateurs explicites pour une gestion plus structurée.