Hello World met het Saga-Patroon: Een Minimalistische Java-Tutorial voor een Pizzabestelling

Geschreven door: bert
| Datum: 01 / 05 / 2025

Het Saga-patroon is een krachtige aanpak voor het beheren van gedistribueerde transacties in moderne applicaties, zoals microservices. Maar hoe begin je met dit patroon als programmeur? In deze tutorial bouwen we een minimale Saga-applicatie in Java, die fungeert als een "Hello World" voor het Saga-patroon. Geen complexe militaire systemen of sci-fi, maar een leuk en herkenbaar voorbeeld: een online pizzabestellingssysteem! We coördineren een bestelling met stappen zoals het controleren van ingrediënten, verwerken van de betaling, en plannen van de bezorging, met compenserende acties als iets misgaat.

Deze tutorial is gericht op programmeurs die hands-on met het Saga-patroon willen starten. Of je nu een beginner bent of een ervaren ontwikkelaar, dit blog leidt je stap voor stap door een eenvoudige, werkende Saga-applicatie. We houden de code minimalistisch, gebruiken duidelijke uitleg, bieden een UML-diagram om alles te visualiseren, en maken het interactief door gebruikersinvoer toe te voegen. Bovendien bespreken we hoe je dit kunt uitbreiden met Kafka voor een meer realistische implementatie. Pak je IDE erbij en laten we een pizza-Saga bouwen!

# Wat is het Saga-Patroon?

Het Saga-patroon is een manier om complexe operaties in gedistribueerde systemen te beheren door ze op te splitsen in kleine, lokale stappen. Elke stap voert een actie uit, publiceert een gebeurtenis, en triggert de volgende stap. Als een stap mislukt, worden compenserende acties uitgevoerd om eerdere stappen ongedaan te maken. In deze tutorial gebruiken we orchestratie, waarbij een centrale coördinator (de Saga Orchestrator) de stappen beheert.

Ons voorbeeld is een online pizzabestelling:

  • Stappen: Controleer ingrediënten, verwerk betaling, plan bezorging.
  • Compensaties: Als de betaling mislukt, geef ingrediënten vrij; als de bezorging niet kan, annuleer de betaling.

Voor technische lezers: Dit is een vereenvoudigde in-memory Saga zonder externe message brokers, maar we bespreken later hoe je dit kunt uitbreiden met Kafka. Voor beginners: Zie het als een recept waarbij elke stap moet slagen, anders draai je de vorige stappen terug (zoals het terugzetten van ingrediënten in de voorraad).

# Wat Gaan We Bouwen?

We bouwen een Java-applicatie die een pizzabestelling coördineert met het Saga-patroon. De applicatie heeft:

  • Drie services: InventoryService (voorraad), PaymentService (betaling), DeliveryService (bezorging).
  • Een Saga Orchestrator: Coördineert de stappen en handelt compensaties af.
  • Gebeurtenissen: Simpele in-memory berichten om stappen te verbinden.
  • Minimale code: Geen databases of complexe afhankelijkheden – puur Java.
  • Interactieve invoer: Gebruikers kunnen kiezen of een stap slaagt of faalt om het proces te demonstreren.

De flow:

  1. Controleer of ingrediënten beschikbaar zijn.
  2. Verwerk de betaling.
  3. Plan de bezorging.
  4. Als een stap mislukt, voer compenserende acties uit (bijvoorbeeld annuleer betaling).

# Vereisten

  • Java 17 (of hoger).
  • Een IDE zoals IntelliJ IDEA of VS Code.
  • Basiskennis van Java (klassen, interfaces, exceptions).
  • Geen externe afhankelijkheden – we houden het simpel!

# Stap-voor-Stap Tutorial

Laten we de applicatie bouwen. Volg deze stappen om de code te schrijven, te begrijpen, en uit te voeren.

# Stap 1: Projectstructuur Aanmaken

Maak een nieuw Java-project met de volgende pakketstructuur:

src/main/java/com/example/pizzasaga/
├── model/
│   ├── Order.java
│   ├── SagaEvent.java
├── service/
│   ├── InventoryService.java
│   ├── PaymentService.java
│   ├── DeliveryService.java
├── orchestrator/
│   ├── SagaOrchestrator.java
├── Main.java

# Stap 2: Definieer het Model

We beginnen met twee eenvoudige klassen: Order (voor bestelgegevens) en SagaEvent (voor gebeurtenissen).

# Order.java

package com.example.pizzasaga.model;

public class Order {
    private final String orderId;
    private final String pizzaType;
    private String status;

    public Order(String orderId, String pizzaType) {
        this.orderId = orderId;
        this.pizzaType = pizzaType;
        this.status = "PENDING";
    }

    public String getOrderId() { return orderId; }
    public String getPizzaType() { return pizzaType; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

# SagaEvent.java

package com.example.pizzasaga.model;

public class SagaEvent {
    private final String type;
    private final Order order;
    private final boolean success;

    public SagaEvent(String type, Order order, boolean success) {
        this.type = type;
        this.order = order;
        this.success = success;
    }

    public String getType() { return type; }
    public Order getOrder() { return order; }
    public boolean isSuccess() { return success; }
}

Uitleg:

  • Order houdt de bestelstatus bij (bijvoorbeeld "PENDING", "COMPLETED", "FAILED").
  • SagaEvent vertegenwoordigt gebeurtenissen zoals InventoryChecked of PaymentProcessed, met een succes/failure-vlag.

# Stap 3: Maak de Services

We definiëren drie services die de stappen van de Saga uitvoeren, elk met een actie en een compensatie. We maken het interactief door de gebruiker te laten kiezen of een stap slaagt.

# InventoryService.java

package com.example.pizzasaga.service;

import com.example.pizzasaga.model.Order;
import com.example.pizzasaga.model.SagaEvent;

import java.util.Scanner;

public class InventoryService {
    public SagaEvent checkInventory(Order order) {
        System.out.println("Checking inventory for " + order.getPizzaType());
        System.out.print("Is inventory available? (y/n): ");
        Scanner scanner = new Scanner(System.in);
        boolean success = scanner.nextLine().trim().toLowerCase().startsWith("y");
        return new SagaEvent("InventoryChecked", order, success);
    }

    public void compensateInventory(Order order) {
        System.out.println("Releasing inventory for " + order.getPizzaType());
    }
}

# PaymentService.java

package com.example.pizzasaga.service;

import com.example.pizzasaga.model.Order;
import com.example.pizzasaga.model.SagaEvent;

import java.util.Scanner;

public class PaymentService {
    public SagaEvent processPayment(Order order) {
        System.out.println("Processing payment for order " + order.getOrderId());
        System.out.print("Does payment succeed? (y/n): ");
        Scanner scanner = new Scanner(System.in);
        boolean success = scanner.nextLine().trim().toLowerCase().startsWith("y");
        return new SagaEvent("PaymentProcessed", order, success);
    }

    public void compensatePayment(Order order) {
        System.out.println("Refunding payment for order " + order.getOrderId());
    }
}

# DeliveryService.java

package com.example.pizzasaga.service;

import com.example.pizzasaga.model.Order;
import com.example.pizzasaga.model.SagaEvent;

import java.util.Scanner;

public class DeliveryService {
    public SagaEvent scheduleDelivery(Order order) {
        System.out.println("Scheduling delivery for order " + order.getOrderId());
        System.out.print("Can delivery be scheduled? (y/n): ");
        Scanner scanner = new Scanner(System.in);
        boolean success = scanner.nextLine().trim().toLowerCase().startsWith("y");
        return new SagaEvent("DeliveryScheduled", order, success);
    }

    public void compensateDelivery(Order order) {
        System.out.println("Canceling delivery for order " + order.getOrderId());
    }
}

Uitleg:

  • Elke service vraagt de gebruiker of de stap slaagt ("y" voor ja, "n" voor nee), wat het interactief maakt.
  • Bij een mislukking wordt een compensatie uitgevoerd (bijvoorbeeld compensateInventory).

# Stap 4: Implementeer de Saga Orchestrator

De SagaOrchestrator coördineert de stappen en handelt fouten af.

# SagaOrchestrator.java

package com.example.pizzasaga.orchestrator;

import com.example.pizzasaga.model.Order;
import com.example.pizzasaga.model.SagaEvent;
import com.example.pizzasaga.service.InventoryService;
import com.example.pizzasaga.service.PaymentService;
import com.example.pizzasaga.service.DeliveryService;

public class SagaOrchestrator {
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final DeliveryService deliveryService;

    public SagaOrchestrator() {
        this.inventoryService = new InventoryService();
        this.paymentService = new PaymentService();
        this.deliveryService = new DeliveryService();
    }

    public void executeSaga(Order order) {
        System.out.println("Starting Saga for order " + order.getOrderId());

        // Stap 1: Controleer voorraad
        SagaEvent inventoryEvent = inventoryService.checkInventory(order);
        if (!inventoryEvent.isSuccess()) {
            order.setStatus("FAILED");
            return;
        }

        // Stap 2: Verwerk betaling
        SagaEvent paymentEvent = paymentService.processPayment(order);
        if (!paymentEvent.isSuccess()) {
            inventoryService.compensateInventory(order);
            order.setStatus("FAILED");
            return;
        }

        // Stap 3: Plan bezorging
        SagaEvent deliveryEvent = deliveryService.scheduleDelivery(order);
        if (!deliveryEvent.isSuccess()) {
            paymentService.compensatePayment(order);
            inventoryService.compensateInventory(order);
            order.setStatus("FAILED");
            return;
        }

        // Succes!
        order.setStatus("COMPLETED");
        System.out.println("Saga completed for order " + order.getOrderId());
    }
}

Uitleg:

  • De orchestrator voert stappen sequentieel uit en controleert het succes van elke SagaEvent.
  • Bij een mislukking voert het compensaties uit voor eerdere stappen in omgekeerde volgorde.
  • Dit is een in-memory implementatie zonder externe systemen voor maximale eenvoud.

# Stap 5: Maak de Hoofdklasse

# Main.java

package com.example.pizzasaga;

import com.example.pizzasaga.model.Order;
import com.example.pizzasaga.orchestrator.SagaOrchestrator;

public class Main {
    public static void main(String[] args) {
        Order order = new Order("123", "Margherita");
        SagaOrchestrator orchestrator = new SagaOrchestrator();
        orchestrator.executeSaga(order);
        System.out.println("Final order status: " + order.getStatus());
    }
}

Uitleg:

  • De Main-klasse maakt een bestelling en start de Saga.
  • Bij uitvoering vraagt de applicatie interactief om invoer voor elke stap.

# Stap 6: Voer de Applicatie Uit

  1. Kopieer de bovenstaande code in je IDE.
  2. Compileer en run Main.java.

Voorbeeld uitvoer (afhankelijk van je invoer):

Starting Saga for order 123
Checking inventory for Margherita
Is inventory available? (y/n): y
Processing payment for order 123
Does payment succeed? (y/n): n
Refunding payment for order 123
Releasing inventory for Margherita
Final order status: FAILED

Of, bij succes:

Starting Saga for order 123
Checking inventory for Margherita
Is inventory available? (y/n): y
Processing payment for order 123
Does payment succeed? (y/n): y
Scheduling delivery for order 123
Can delivery be scheduled? (y/n): y
Saga completed for order 123
Final order status: COMPLETED

Uitleg:

  • Je kunt de uitkomst beïnvloeden door "y" (ja) of "n" (nee) in te voeren.
  • De Saga voert stappen uit en compenseert bij mislukkingen.

# UML-Diagram: De Structuur Visualiseren

Hier is een UML-klassendiagram om de structuur te visualiseren:

+----------------+           +----------------+
|    Order       |           |   SagaEvent    |
|----------------|           |----------------|
| -orderId       |           | -type          |
| -pizzaType     |           | -order         |
| -status        |           | -success       |
| +getOrderId()  |           | +getType()     |
| +getPizzaType()|           | +getOrder()    |
| +getStatus()   |           | +isSuccess()   |
| +setStatus()   |           +----------------+
+----------------+
       ^
       |
+--------------------+       +-----------------------+
| SagaOrchestrator   |<>---->| InventoryService      |
|--------------------|       |-----------------------|
| -inventoryService  |       | +checkInventory()     |
| -paymentService    |       | +compensateInventory()|
| -deliveryService   |       +-----------------------+
| +executeSaga()     |
+--------------------+       +---------------------+
       |                     | PaymentService      |
       |                     |---------------------|
       |                     | +processPayment()   |
       |                     | +compensatePayment()|
       |                     +---------------------+
       |
       |                     +----------------------+
       |                     | DeliveryService      |
       |                     |----------------------|
       |                     | +scheduleDelivery()  |
       |                     | +compensateDelivery()|
       |                     +----------------------+

Uitleg:

  • De SagaOrchestrator heeft afhankelijkheden op de drie services.
  • Order en SagaEvent zijn eenvoudige datamodellen.
  • Elke service implementeert een actie en compensatie.

# Implementatie met Kafka: Een Stap Verder

Tot nu toe hebben we een in-memory Saga gebouwd, wat perfect is voor een eerste kennismaking. Maar in een echte gedistribueerde omgeving wil je een message broker zoals Apache Kafka gebruiken om gebeurtenissen tussen services te communiceren. Hier is hoe je dit kunt uitbreiden:

# 1. Voeg Kafka toe:

Voeg de Kafka-dependency toe aan je project (bijvoorbeeld via Maven):

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.6.0</version>
</dependency>

Start een Kafka-server (bijvoorbeeld via Docker) en maak een topic saga-events.

# 2. Pas SagaEvent aan:

Maak SagaEvent serialiseerbaar voor Kafka:

import java.io.Serializable;

public class SagaEvent implements Serializable {
    // ... (bestaande code)
}

# 3. Gebruik Kafka in Services:

Laat services gebeurtenissen publiceren naar Kafka:

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;

public class InventoryService {
    private final KafkaProducer<String, SagaEvent> producer;

    public InventoryService() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
        this.producer = new KafkaProducer<>(props);
    }

    public SagaEvent checkInventory(Order order) {
        // ... (bestaande logica)
        SagaEvent event = new SagaEvent("InventoryChecked", order, success);
        producer.send(new ProducerRecord<>("saga-events", event.getType(), event));
        return event;
    }
}

# 4. Pas de Orchestrator aan:

Laat de SagaOrchestrator consumeren van Kafka en stappen coördineren op basis van gebeurtenissen:

import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.ConsumerRecords;

public class SagaOrchestrator {
    private final KafkaConsumer<String, SagaEvent> consumer;

    public SagaOrchestrator() {
        // Kafka consumer config
        // ... (vergelijkbare setup als producer)
    }

    public void executeSaga(Order order) {
        // Start Saga en luister naar gebeurtenissen via Kafka
    }
}

Uitleg:

  • Kafka fungeert als een EventBus, waarbij services gebeurtenissen publiceren en consumeren.
  • Dit maakt de applicatie echt gedistribueerd, geschikt voor microservices.
  • Voor een volledige implementatie raad ik aan om een Kafka-tutorial te volgen of libraries zoals Spring Kafka te gebruiken.

# Waarom is Dit een Goede Start?

Deze minimalistische Saga-applicatie is perfect voor beginners omdat:

  1. Eenvoud: Geen externe afhankelijkheden in de basisversie – puur Java.
  2. Interactief: Gebruikersinvoer maakt het leerproces dynamisch en boeiend.
  3. Duidelijkheid: De code focust op de kern van het Saga-patroon: stappen, gebeurtenissen, en compensaties.
  4. Praktisch: Het pizzabestellingsscenario is herkenbaar en maakt abstracte concepten tastbaar.
  5. Uitbreidbaar: De Kafka-uitbreiding toont hoe je naar een productieklare oplossing kunt groeien.

Voor technische lezers: Dit is een in-memory Saga, maar de Kafka-uitbreiding maakt het schaalbaar voor echte microservices-architecturen. Voor beginners: Het is een eerste stap om het Saga-patroon te begrijpen zonder overweldigd te raken.

# Hoe Verder na Deze Hello World?

Zodra je deze basisversie beheerst, kun je uitbreiden:

  1. Voeg een echte EventBus toe: Gebruik Kafka of RabbitMQ voor gedistribueerde communicatie.
  2. Implementeer choreografie: Laat services direct op gebeurtenissen reageren zonder een centrale orchestrator.
  3. Voeg persistentie toe: Sla gebeurtenissen op in een database voor traceerbaarheid.
  4. Voeg meer interactiviteit toe: Laat gebruikers pizza's kiezen of meerdere bestellingen plaatsen.
  5. Gebruik frameworks: Verken libraries zoals Axon Framework of Spring Cloud voor Saga-ondersteuning.

# Praktische Tips voor je Eerste Saga

  1. Houd stappen eenvoudig: Elke stap moet één duidelijke actie uitvoeren (bijvoorbeeld checkInventory).
  2. Maak compensaties robuust: Zorg ervoor dat ze altijd werken, zelfs bij herhaalde uitvoering.
  3. Log gebeurtenissen: Print of sla gebeurtenissen op om de flow te debuggen.
  4. Test verschillende scenario's: Gebruik de interactieve invoer om succes- en faalscenario's te testen.
  5. Begin met orchestratie: Het is eenvoudiger dan choreografie voor je eerste Saga.

# Conclusie

Met deze Hello World Saga-applicatie heb je je eerste stap gezet in het Saga-patroon! Door een eenvoudige pizzabestelling te coördineren, heb je geleerd hoe je stappen, gebeurtenissen, en compenserende acties implementeert in Java. De interactieve invoer en het herkenbare scenario maken het een ideale start voor programmeurs die het Saga-patroon willen verkennen zonder complexiteit.

Probeer de code uit, experimenteer met succes- en faalscenario's, en breid de applicatie uit met Kafka of andere features. Of je nu een microservices-architectuur bouwt of gewoon nieuwsgierig bent naar gedistribueerde transacties, deze tutorial geeft je een solide basis. Bestel die virtuele pizza, start je Saga, en ontdek de kracht van moderne software-architectuur!

# Bronnen:

  • Chris Richardson, Microservices Patterns.
  • Algemene concepten van het Saga-patroon en event-driven architecturen.
  • Java-documentatie voor objectgeoriënteerd programmeren.
  • Apache Kafka-documentatie voor gedistribueerde messaging.

Reacties (0 )

Geen reacties beschikbaar.