ADR-0005: Saga mit Kompensation für Überweisungen
Status
Akzeptiert, 2026-08-08
Kontext
Eine Überweisung verändert den Saldo von zwei Konten, die in accounts-service leben (siehe
ADR-0003, Datenbank pro Service) - payments-service selbst besitzt keinen eigenen Zugriff auf
Kontostände. Eine verteilte ACID-Transaktion über zwei Services hinweg (Two-Phase-Commit) würde
eine enge technische Kopplung beider Services erzwingen (gemeinsamer Transaktionskoordinator,
Sperren über Servicegrenzen hinweg) - genau das, was Microservice-Grenzen eigentlich vermeiden
sollen (siehe ADR-0001 dieses Repos, allgemeiner: [microservices.io/patterns/data/saga.html]).
Entscheidung
payments-service orchestriert eine Saga mit explizitem Kompensationsschritt
(InitiateTransferUseCase):
Transferwird alsINITIATEDpersistiert, bevor irgendein Konto berührt wird.- Abbuchung beim Quellkonto (
POST /accounts/{iban}/withdrawalsin accounts-service). Schlägt das fehl (z. B. keine Deckung, Konto nicht aktiv):Transfer→FAILED, Ende - es gibt nichts zu kompensieren, da noch keine Kontobewegung stattfand. - Gutschrift beim Zielkonto (
POST /accounts/{iban}/deposits). Schlägt das fehl (z. B. Zielkonto inzwischen geschlossen/eingefroren): die bereits erfolgte Abbuchung wird durch eine Gegenbuchung auf das Quellkonto rückgängig gemacht,Transfer→COMPENSATED. - Beide Schritte erfolgreich: doppelte Buchführung wird gebucht (
LedgerBooking),Transfer→COMPLETED.
Die Buchführung selbst (Debit-/Credit-Eintrag) erfolgt nach beiden erfolgreichen Kontobewegungen, nicht parallel dazu - das Ledger dokumentiert nur tatsächlich abgeschlossene Überweisungen.
Konsequenzen
Positiv
- Kein verteilter Transaktionskoordinator, keine service-übergreifenden Sperren -
accounts-servicebleibt vollständig unabhängig betreib- und deploybar. - Jeder Zwischenzustand (
INITIATED,FAILED,COMPENSATED,COMPLETED) ist explizit modelliert und überGET /transfers/{id}abfragbar - kein "hängender", unklarer Zustand. - Der Saga-Ablauf ist als eigener, isoliert testbarer Anwendungsfall (
InitiateTransferUseCase) formuliert - alle drei Ausgänge lassen sich per Outside-In-TDD mit gemocktemAccountMovementPorteinzeln erzwingen (sieheInitiateTransferUseCaseTest).
Negativ / Trade-offs
- Kein echtes ACID: zwischen Schritt 2 (Abbuchung erfolgreich) und Schritt 3 (Gutschrift noch ausstehend) existiert ein kurzes Zeitfenster, in dem das Geld "unterwegs" ist (weder auf dem Quell- noch sichtbar auf dem Zielkonto). Für dieses Lernprojekt akzeptiert; ein Produktionssystem würde zusätzlich einen Reconciliation-Prozess für unterbrochene Sagas (z. B. Prozessabsturz zwischen Schritt 2 und 3) ergänzen, der hier bewusst nicht abgebildet ist.
- Die Kompensation selbst kann theoretisch ebenfalls fehlschlagen (z. B.
accounts-servicein genau diesem Moment nicht erreichbar) - in diesem Lernprojekt nicht mit Retry/Dead-Letter abgefedert, siehe ADR-0004 (schlanke Infrastruktur). - Zwei synchrone REST-Aufrufe pro Überweisung (Abbuchung, Gutschrift) statt eines einzelnen DB-Commits - höhere Latenz, akzeptiert zugunsten der Service-Unabhängigkeit.
Alternativen erwogen
- Zwei-Phasen-Commit über eine verteilte Transaktion: verworfen wegen der oben beschriebenen engen Kopplung und weil die meisten modernen Messaging-/HTTP-Infrastrukturen (hier: REST) kein 2PC-Protokoll unterstützen.
- Choreografie statt Orchestrierung (Services reagieren rein event-getrieben auf Kafka-Events
ohne zentralen Koordinator): für nur zwei beteiligte Schritte (Ab-/Zubuchung) hier als
unnötige zusätzliche Komplexität bewertet - Orchestrierung macht den Ablauf an einer Stelle
(
InitiateTransferUseCase) linear nachvollziehbar, was für ein Lehrprojekt den Saga-Mechanismus klarer zeigt als verteilte Event-Handler.