Saga-patroon: gedistribueerde transacties met choreografie en orkestratie
Bij microservices bestaat er niet zoiets als BEGIN TRANSACTION ... COMMIT dat de grenzen van de dienstverlening overschrijdt.
Wanneer een handeling betrekking heeft op de Besteldienst, de Voorraaddienst en de Betaaldienst,
Hoe zorgen we ervoor dat ze alle drie succesvol zijn, of dat geen enkele effectief is?
De Saga-patroon is het antwoord: elke dienst voert zijn eigen lokale transactie uit
en, in geval van mislukking, de compenserende transacties om ongedaan te maken wat al is gedaan.
Waarom 2PC niet werkt in microservices
Il Tweefasige committering (2PC) is het klassieke protocol voor gedistribueerde transacties: een coördinator vraagt alle deelnemers zich voor te bereiden (fase voorbereiden), bestel vervolgens de commit. Dit werkt goed voor homogene databases (bijvoorbeeld twee PostgreSQL-instanties), maar is problematisch bij microservices:
- Koppeling: Alle diensten moeten beschikbaar zijn tijdens de transactie
- Prestatie: bronnen blijven vergrendeld totdat ze worden vastgelegd (gedistribueerde vergrendelingen)
- Beperkte ondersteuning: Veel cloudservices, REST API's en NoSQL-databases ondersteunen geen 2PC
- Schaalbaarheid: de coördinator wordt een single point of faillment
Het Saga-patroon vervangt één grote gedistribueerde transactie door één reeks lokale transacties, elk afzonderlijk voltooid, gecoördineerd via evenementen of een orkestrator.
Voorbeeld: E-commerce-aankoopsaga
Saga-stappen voor een aankoop:
- Orde creëren (Bestelservice) → OrdineCreato
- Reserveer inventaris (Inventarisservice) → Gereserveerde voorraad
- Betaling verwerken (Betaalservice) → Betaling bevestigd
- Bevestig bestelling (Bestelservice) → Bestelling bevestigd
Als de betaling mislukt, zijn de compenserende transacties:
- Voorraad vrijgeven (Inventarisservice)
- Bestelling annuleren (Bestelservice)
Choreografie Saga: Service-autonomie
In de Choreografie Saga, er is geen centrale orkestrator. Elke dienst luistert naar de gebeurtenissen van de vorige dienst en reageert dienovereenkomstig, op zijn beurt nieuwe evenementen opleveren. De coördinatielogica is gedistribueerd tussen diensten: elk kent alleen zijn eigen stap en de gebeurtenissen waarop het reageert.
Pro: maximale ontkoppeling, geen single point of Failure, eenvoudig te implementeren. Tegen: moeilijk te debuggen (saga-logica wordt gedistribueerd), geen zichtbaarheid Centraal in de stand van de saga staat het risico van cycli van slecht beheerde gebeurtenissen.
// CHOREOGRAPHY SAGA con Spring Kafka
// ===== STEP 1: Servizio Ordini - crea l'ordine =====
@Service
public class OrdiniService {
private final KafkaTemplate<String, Object> kafkaTemplate;
public String creaOrdine(CreaOrdineCommand cmd) {
String ordineId = UUID.randomUUID().toString();
// Salva l'ordine in stato PENDING
Ordine ordine = Ordine.builder()
.id(ordineId)
.clienteId(cmd.getClienteId())
.prodotti(cmd.getProdotti())
.stato(StatoOrdine.PENDING)
.build();
ordineRepository.save(ordine);
// Pubblica evento: il Servizio Inventario lo ascolterà
OrdineCreato event = new OrdineCreato(ordineId, cmd.getProdotti());
kafkaTemplate.send("ordini-events", ordineId, event);
return ordineId;
}
// Ascolta la conferma del pagamento per finalizzare l'ordine
@KafkaListener(topics = "pagamenti-events", groupId = "ordini-service")
public void onPagamento(ConsumerRecord<String, Object> record) {
if (record.value() instanceof PagamentoConfermato event) {
ordineRepository.updateStato(event.getOrdineId(), StatoOrdine.CONFERMATO);
kafkaTemplate.send("ordini-events", event.getOrdineId(),
new OrdineConfermato(event.getOrdineId()));
}
if (record.value() instanceof PagamentoFallito event) {
// Compensating: annulla l'ordine
ordineRepository.updateStato(event.getOrdineId(), StatoOrdine.ANNULLATO);
}
}
}
// ===== STEP 2: Servizio Inventario - riserva i prodotti =====
@Service
public class InventarioService {
@KafkaListener(topics = "ordini-events", groupId = "inventario-service")
public void onOrdinCreato(ConsumerRecord<String, Object> record) {
if (record.value() instanceof OrdineCreato event) {
try {
// Tenta la prenotazione dell'inventario
prenotaInventario(event.getOrdineId(), event.getProdotti());
// Successo: pubblica evento per il Servizio Pagamenti
kafkaTemplate.send("inventario-events", event.getOrdineId(),
new InventarioRiservato(event.getOrdineId(), event.getTotale()));
} catch (InventarioInsufficienterException e) {
// Fallimento: pubblica evento di fallimento
// Il Servizio Ordini annullerà l'ordine
kafkaTemplate.send("inventario-events", event.getOrdineId(),
new InventarioNonDisponibile(event.getOrdineId(), e.getMessage()));
}
}
// Compensating: rilascia l'inventario se il pagamento fallisce
if (record.value() instanceof PagamentoFallito event) {
rilasciaInventario(event.getOrdineId());
}
}
}
// ===== STEP 3: Servizio Pagamenti - processa il pagamento =====
@Service
public class PagamentiService {
@KafkaListener(topics = "inventario-events", groupId = "pagamenti-service")
public void onInventarioRiservato(ConsumerRecord<String, Object> record) {
if (record.value() instanceof InventarioRiservato event) {
try {
processaPagamento(event.getOrdineId(), event.getTotale());
kafkaTemplate.send("pagamenti-events", event.getOrdineId(),
new PagamentoConfermato(event.getOrdineId()));
} catch (PagamentoRifiutatoException e) {
kafkaTemplate.send("pagamenti-events", event.getOrdineId(),
new PagamentoFallito(event.getOrdineId(), e.getMessage()));
}
}
}
}
Orchestration Saga: gecentraliseerde controle
In de'Orkestratie-saga, een centraal onderdeel genaamd Saga-orkestrator (of Process Manager) coördineert het gehele traject. De orkestrator kent alle stappen, stuur een opdracht naar services en wacht op hun reactiegebeurtenissen. In geval van mislukking, de orkestrator verzendt expliciet compensatieopdrachten.
Pro: gecentraliseerde en zichtbare logica, gemakkelijk te debuggen, expliciete sagastatus, nauwkeurigere foutafhandeling. Tegen: grote koppeling, de orkestrator kent alle betrokken diensten.
// ORCHESTRATION SAGA con Axon Framework (Java)
// Axon gestisce automaticamente la persistenza dello stato della saga
import org.axonframework.saga.*;
import org.axonframework.eventhandling.*;
@Saga
public class AcquistoSaga {
@Autowired
private transient CommandGateway commandGateway;
// Lo stato della saga persiste automaticamente tra gli step
private String ordineId;
private List<ProdottoOrdinato> prodotti;
private BigDecimal totale;
// STEP 1: Avvio della saga quando viene creato un ordine
@StartSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineCreato event) {
this.ordineId = event.getOrdineId();
this.prodotti = event.getProdotti();
this.totale = event.getTotale();
// Invia comando al servizio inventario
commandGateway.send(new RiservaInventarioCommand(
ordineId,
prodotti
));
}
// STEP 2: Inventario riservato con successo - procedi con il pagamento
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioRiservato event) {
commandGateway.send(new ProcessaPagamentoCommand(
ordineId,
totale,
"CARTA_CREDITO"
));
}
// STEP 3: Pagamento confermato - finalizza l'ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(PagamentoConfermato event) {
commandGateway.send(new ConfermaOrdineCommand(ordineId));
}
// Fine saga (successo)
@EndSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineConfermato event) {
System.out.println("Saga completata con successo per ordine: " + ordineId);
}
// ===== COMPENSATING TRANSACTIONS =====
// Pagamento fallito: rilascia inventario, poi annulla ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(PagamentoFallito event) {
// Compensating step 1: rilascia inventario
commandGateway.send(new RilasciaInventarioCommand(ordineId, prodotti));
}
// Inventario rilasciato - annulla l'ordine
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioRilasciato event) {
// Compensating step 2: annulla l'ordine
commandGateway.send(new AnnullaOrdineCommand(ordineId, "Pagamento fallito"));
}
// Inventario non disponibile - annulla direttamente
@SagaEventHandler(associationProperty = "ordineId")
public void on(InventarioNonDisponibile event) {
commandGateway.send(new AnnullaOrdineCommand(ordineId, "Inventario insufficiente"));
}
// Fine saga per percorso di fallimento
@EndSaga
@SagaEventHandler(associationProperty = "ordineId")
public void on(OrdineAnnullato event) {
System.out.println("Saga fallita/annullata per ordine: " + ordineId + ". Motivo: " + event.getMotivo());
}
}
Saga met AWS Step-functies (serverloos)
Voor serverloze architecturen op AWS, Stap-functies het is de beheerde service om Orchestration Saga te implementeren. Elke stap is een Lambda-functie of een AWS-actie, en Step Functions beheert per definitie automatisch de status, nieuwe pogingen en compensaties in Amazon States Language (ASL).
{
"Comment": "Saga acquisto e-commerce con compensating transactions",
"StartAt": "CreaOrdine",
"States": {
"CreaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:crea-ordine",
"Next": "RiservaInventario",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "FallimentoSaga"
}
]
},
"RiservaInventario": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:riserva-inventario",
"Next": "ProcessaPagamento",
"Catch": [
{
"ErrorEquals": ["InventarioInsufficienterException"],
"Next": "CompensaAnnullaOrdine"
}
]
},
"ProcessaPagamento": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:processa-pagamento",
"Retry": [
{
"ErrorEquals": ["TransientPaymentError"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Next": "ConfermaOrdine",
"Catch": [
{
"ErrorEquals": ["PagamentoRifiutatoException"],
"Next": "CompensaRilasciaInventario"
}
]
},
"ConfermaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:conferma-ordine",
"End": true
},
"CompensaRilasciaInventario": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:rilascia-inventario",
"Next": "CompensaAnnullaOrdine"
},
"CompensaAnnullaOrdine": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:123456:function:annulla-ordine",
"Next": "FallimentoSaga"
},
"FallimentoSaga": {
"Type": "Fail",
"Error": "SagaFailed",
"Cause": "La transazione distribuita e fallita, compensazioni eseguite"
}
}
}
Idempotentie bij het compenseren van transacties
Er moeten compenserende transacties zijn idempotent: indien meerdere keren aangeroepen (vanwege een nieuwe poging van het netwerk of de Orchestrator), moet het resultaat hetzelfde zijn. De meest gebruikelijke techniek is het gebruik van a idempotentie sleutel: de orderId + het bewerkingstype vormen een unieke sleutel die dubbele bewerkingen voorkomt.
// RilasciaInventarioHandler.java - Compensating transaction idempotente
@CommandHandler
public void handle(RilasciaInventarioCommand cmd) {
// Controllo idempotenza: questa compensazione e gia stata eseguita?
String idempotencyKey = cmd.getOrdineId() + ":RILASCIA_INVENTARIO";
if (compensazioniEseguite.contains(idempotencyKey)) {
System.out.println("Compensazione gia eseguita per " + idempotencyKey + ", skip");
return;
}
// Esegui la compensazione
for (ProdottoOrdinato prodotto : cmd.getProdotti()) {
inventarioRepository.incrementaDisponibilita(
prodotto.getProductId(),
prodotto.getQuantita()
);
}
// Segna la compensazione come eseguita
compensazioniEseguite.add(idempotencyKey);
// Pubblica evento per continuare la catena di compensazione
eventBus.publish(new InventarioRilasciato(cmd.getOrdineId()));
}
Saga State Tracking: inzicht in de staat
Een van de punten van kritiek op Orchestration Saga is de moeilijkheid om de staat te volgen in hoogvolumesystemen. Een expliciete saga-trackingtabel lost dit probleem op:
-- Tabella di tracking per le saga in corso
CREATE TABLE saga_state (
saga_id VARCHAR(36) PRIMARY KEY,
saga_type VARCHAR(100) NOT NULL,
ordine_id VARCHAR(36) NOT NULL,
current_step VARCHAR(100) NOT NULL,
stato VARCHAR(50) NOT NULL, -- IN_CORSO, COMPLETATA, FALLITA, COMPENSATA
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
error_message TEXT
);
-- Query utili per monitoring
-- Saga in corso da piu di 5 minuti (potenzialmente bloccate)
SELECT * FROM saga_state
WHERE stato = 'IN_CORSO'
AND started_at < NOW() - INTERVAL '5 minutes';
-- Distribuzione per step corrente
SELECT current_step, COUNT(*) as count
FROM saga_state WHERE stato = 'IN_CORSO'
GROUP BY current_step;
Vergelijking: choreografie versus orkestratie
| Ik wacht | Choreografie | Orkestratie |
|---|---|---|
| Koppeling | Laag (evenementen) | Medium (orkestrator kent diensten) |
| Zichtbaarheid van status | Moeilijk (gedistribueerd) | Hoog (gecentraliseerd) |
| Foutopsporing | Moeilijk (gedistribueerde logica) | Gemakkelijk (expliciete staat) |
| Complexiteit van diensten | Hoog (elke dienst kent de vergoedingen) | Laag (eenvoudige services, logica in Orchestrator) |
| Schaalbaarheid | Hoog (geen enkele punten) | Medium (orkestrator op schaal) |
| Ideale gebruikssituatie | Weinig stappen, autonome diensten, onafhankelijke teams | Veel stappen, complexe logica, noodzakelijke zichtbaarheid |
Volgende stappen in de serie
- Artikel 6 – AWS EventBridge: Choreography Saga gebruikt een berichtenbus om evenementen uitwisselen. EventBridge is de ideale door AWS beheerde bus voor intelligente routering van evenementen tussen Lambda en SQS.
- Artikel 10 – Outbox-patroon: Zorg voor atomaire verzending van Saga-gebeurtenissen samen met de lokale database-update.
Link met andere series
- Evenementsourcing + CQRS (artikel 4): elke stap van de Saga produceert gebeurtenissen die kan worden gebruikt als bron van waarheid om de totale staat te reconstrueren. Axon Framework ondersteunt standaard de combinatie Saga + Event Sourcing.
- Apache Kafka (serie 38): Kafka is de ideale berichtenbus voor Choreografie Saga in productie, met garanties voor duurzaamheid en ordening per sleutelpartitie.







