Referenzarchitektur: Datenschicht für Microservices
Bei Microservices ist die wichtigste Entscheidung die Datenhoheit: Jeder Service muss seine Daten besitzen (Database-per-Service) und darf nicht direkt auf die Tabellen eines anderen Service zugreifen. Dieser Baum geht Schritt für Schritt das Shared-DB-Anti-Pattern, serviceübergreifende Konsistenz ohne verteilte Transaktionen (Saga + Outbox), Datensynchronisierung per CDC und serviceübergreifende Abfragen (API Composition vs. CQRS-Lesemodell) durch. Ziel ist, Services wirklich unabhängig und separat deploybar zu halten.
Wie besitzen Services Daten: eine gemeinsame DB oder eine pro Service?
Datenisolation ist das Rückgrat einer Microservice-Architektur. Nutzen Services eine einzige gemeinsame Datenbank (eine versteckte Abhängigkeit), hat jeder Service eine eigene Datenbank (das richtige Muster), oder liegt die eigentliche Herausforderung bei serviceübergreifender Konsistenz/Abfragen? Kläre zuerst den aktuellen Stand.
- Alle Services nutzen eine einzige gemeinsame DB
- Jeder Service hat eine eigene DB, aber Abläufe brauchen serviceübergreifende Konsistenz
- Jeder Service hat eine eigene DB, aber kombinierte Abfragen/Berichte sind nötig
Wie hältst du einen serviceübergreifenden Ablauf konsistent?
Für eine Operation über mehrere Services (Bestellung anlegen → Zahlung einziehen → Bestand verringern) ist eine klassische verteilte Transaktion (2PC) fragil und skaliert nicht. Teile den Ablauf stattdessen in Schritte aus lokaler Transaktion + Ereignis auf (Saga) und veröffentliche Ereignisse zuverlässig (Outbox). Mit welchem Problem hast du es zu tun?
- Ich versuche, einen Ablauf über mehrere Services atomar zu halten (2PC?)
- Ich bekomme lokales Schreiben und Ereignisveröffentlichung nicht konsistent (Dual Write)
Wie bedienst du eine Abfrage, die Daten aus mehreren Services anzeigt?
Sobald Daten auf Services verteilt sind, kannst du keine kombinierte Ansicht mit einem einzigen Join aufbauen. Es gibt zwei Hauptansätze: zur Anfragezeit die APIs der betreffenden Services aufrufen und die Ergebnisse zusammenführen (API Composition) oder ein vorab verknüpftes, über Events gespeistes Lesemodell pflegen (CQRS). Häufigkeit und Komplexität der Abfrage bestimmen die Wahl.
- Einfacher, seltener Join; einige Services aufzurufen und zusammenzuführen reicht
- Komplexe/häufige kombinierte Abfrage; ein fertiges Lesemodell wird benötigt
Empfehlung: Database-per-Service (gemeinsame DB aufteilen)
Services, die sich eine Datenbank teilen, sind eine versteckte und gefährliche Abhängigkeit: Die Schemaänderung eines Service bricht die anderen, die Zuständigkeit für Tabellen wird unklar und unabhängiges Deployment wird unmöglich. Jeder Service muss seine Daten besitzen.
Empfehlung: Saga-Muster (verteilte Transaktionen vermeiden)
Versuche nicht, eine Operation über mehrere Services mit einer einzigen verteilten Transaktion (2PC) atomar zu machen; 2PC hält Sperren und bringt Fragilität an einem einzelnen Punkt und geringe Verfügbarkeit. Teile den Ablauf stattdessen in Schritte, die jeweils eine lokale Transaktion im eigenen Service sind, und mach sie bei einem Fehler mit Kompensationsschritten rückgängig.
Empfehlung: Transactional Outbox (die Dual-Write-Falle)
Will ein Service sowohl in seine eigene DB schreiben als auch ein Ereignis veröffentlichen, ist es die „Dual Write“-Falle, beides getrennt zu tun: Gelingt das eine und scheitert das andere, bleibt das System inkonsistent. Die Lösung ist, das Ereignis in DERSELBEN lokalen Transaktion wie die Geschäftsdaten in eine Outbox-Tabelle zu schreiben und es dann mit einem separaten Prozess zu veröffentlichen.
Empfehlung: API Composition (zum Abfragezeitpunkt zusammenführen)
Wo kombinierte Daten nötig sind, ruft der Aufrufer (z. B. ein Gateway/BFF) die APIs der relevanten Services auf und führt die Ergebnisse im Speicher zusammen. Das ist die einfachste Lösung für einfache Joins über wenige Services; es wird keine zusätzliche Datenkopie vorgehalten.
Empfehlung: CQRS-Lesemodell (durch Ereignisse gespeist)
Halte für komplexe und häufige kombinierte Abfragen ein vorab gejointes, abfrageoptimiertes Lesemodell (Materialized View/denormalisierte Kopie) vor, das von den Ereignissen der Services gespeist wird. Die Schreibseite bleibt in den Services; die Leseseite liefert fertige Daten schnell von einer Stelle.