Das Microservices-Muster bot unabhängige Deploybarkeit, Team-Autonomie und unbegrenzte Skalierbarkeit.
Viele Organisationen integrierten es in den Zeitraum zwischen 2015 und 2020.
Mit Beginn der 2020er-Jahre konsolidieren diese Unternehmen Dienste, reduzieren die operationelle Komplexität und stellen eine Frage, die vor einem Jahrzehnt ketzerisch geklungen hätte: Hätten wir einen Monolithen bauen sollen?
Dies ist keine Ablehnung verteilter Systeme.
Es ist vielmehr eine ehrlichere Bewertung ihrer Kosten, und die Erkenntnis, dass der modulare Monolith, gut gemacht, die meisten Vorteile von Microservices ohne den größten Teil des Overheads liefern kann.
Was mit Microservices schiefgelaufen ist
Microservices sind nicht von Natur aus schlecht. Sie sind für bestimmte Probleme tatsächlich die richtige Wahl. Aber das Muster wurde weit verbreitet, ohne dass die notwendigen organisatorischen und technischen Voraussetzungen gegeben waren.
Die unterschätzten Kosten lauteten:
Komplexität verteilter Systeme. Jede Service‑Grenze führt Netzwerkaufrufe, Serialisierung, Teilausfälle und eventuelle Konsistenz ein. Ein Monolith, der abstürzt, ist ein einzelner Ausfall. Ein verteiltes System, das teilweise ausfällt, ist ein Debugging‑Albtraum. Martin Fowlers „Microservices‑Prämie“, die zusätzlichen Komplexitätskosten der Verteilung, lohnt sich nur, wenn die Vorteile real und messbar sind. Denn die zusätzliche Komplexität erhöht den Aufwand für Fehlersuche und Wartung exponentiell, sobald Anfragen mehrere Dienste durchlaufen.
Operationeller Overhead. Jeder Dienst benötigt seine eigene CI/CD‑Pipeline, sein eigenes Container‑Image, Health‑Checks, Logging‑Konfiguration, Alerting‑Regeln und On‑Call‑Rotation. Ein System mit 50 Diensten hat die 50‑fache operationelle Oberfläche eines Monolithen. Teams ohne starke Platform‑Engineering‑Fähigkeiten verbrachten dabei mehr Zeit mit Infrastruktur als mit Produktentwicklung, was zu einer Situation führen kann, in der der operationelle Aufwand so groß wird, dass er den Fokus auf der Wertschöpfung für den Endbenutzer behindert. In der Praxis bedeutet das, dass Feature‑Entwicklungen um Wochen verzögert werden, weil Ressourcen für das Pflegen von Dockerfiles, Helm‑Charts und Monitoring‑Dashboards gebunden sind.
Vorzeitige Dekomposition. Dienste wurden entlang technischer Linien aufgeteilt (ein „User‑Service“, ein „Notification‑Service“) und nicht entlang Domain‑Linien. Wenn ein Feature Änderungen an drei Diensten erforderte, verschwand die vermeintliche Unabhängigkeit, es wurde einfach ein verteilter Monolith mit allen Koordinationskosten und ohne die Vorteile. Jede Cross‑Service‑Transaktion erzeugt Netzwerklatenz und erfordert kompensierende Aktionen, wodurch die versprochene Geschwindigkeit der unabhängigen Entwicklung praktisch zunichte wird.
Datenkonsistenz. Verteilte Transaktionen sind schwierig. Sagas und eventuelle Konsistenz sind korrekte Lösungen, aber sie erfordern sorgfältiges Design und fügen jeder dienstübergreifenden Operation erhebliche Komplexität hinzu. Viele Teams entdeckten das erst, nachdem sie ihre Datenbank aufgeteilt hatten. Diese Komplexität kann zu Dateninkonsistenzen und Zuverlässigkeitsproblemen führen, was es schwierig macht, sicherzustellen, dass das System wie geplant funktioniert. Ein typisches Symptom ist, dass ein Auftrag in einem Dienst gespeichert, im anderen aber nicht reflektiert wird, weil die Commit‑Koordination fehlgeschlagen ist.
Der modulare Monolith
Ein modularer Monolith ist eine einzelne deploybare Einheit, die in klar definierte, lose gekoppelte Module organisiert ist. Jedes Modul besitzt seine Domain‑Logik und seine Daten. Module kommunizieren über explizite Schnittstellen, nicht über gemeinsame Datenbanktabellen oder direkte Objektreferenzen.
Die Schlüsselerkenntnis: Die Vorteile von Microservices, Team‑Autonomie, unabhängige Entwicklung, klare Domain‑Grenzen, kommen primär aus Modularität, nicht aus Verteilung. Ein gut modularisierter Monolith kann die meisten dieser Vorteile ohne den operationellen Overhead eines verteilten Systems bieten. Dieser Ansatz erlaubt es Teams, die Vorteile von Microservices zu nutzen, ohne die zusätzliche Komplexität und den betrieblichen Aufwand. Da der Code in einem einzigen Prozess läuft, ist die Latenz minimal und das Testen bleibt unkompliziert.
Modularer Monolith Struktur:
src/
modules/
orders/
OrderService.ts ← öffentliche Schnittstelle
OrderRepository.ts ← besitzt seinen Datenzugriff
OrderEvents.ts ← Domain‑Events
internal/ ← private Implementierung
inventory/
InventoryService.ts
InventoryRepository.ts
internal/
notifications/
NotificationService.ts
internal/
shared/
events/ ← modulübergreifender Event‑Bus
types/ ← nur gemeinsame Werttypen
Die Regel: Module kommunizieren nur über ihre öffentlichen Schnittstellen. Kein Modul greift in das internal/‑Verzeichnis eines anderen Moduls. Kein Modul fragt direkt die Datenbanktabelle eines anderen Moduls ab. Diese strikte Einhaltung klarer und gut definierter Schnittstellen hilft dabei, die Integrität und Modularität des Systems aufrechtzuerhalten und es zu erleichtern, es im Laufe der Zeit effektiv zu verwalten und zu skalieren. Damit bleibt die interne Implementierung eines Moduls austauschbar, ohne dass andere Teile des Systems angepasst werden müssen.
Spring Modulith
Spring Modulith ist ein Framework für den Bau modularer Monolithen in Java. Es erzwingt Modulgrenzen auf Paketebene und bietet Werkzeuge zur Überprüfung, dass Module ihre deklarierten Schnittstellen nicht verletzen.
// Spring Modulith: Modulgrenzendurchsetzung
@ApplicationModule(allowedDependencies = "shared")
package com.example.orders;
// Das schlägt beim Architekturtest fehl, wenn orders/ aus inventory/internal/ importiert
Spring Modulith unterstützt die Veröffentlichung von Domain-Events zwischen Modulen über Spring's Application-Event-Mechanismus. Eine spätere Ersetzung durch einen echten Message-Broker ist erforderlich, wenn das Modul in einen Dienst extrahiert wird. Dies erschwert den Übergang zu einer Microservices-Architektur nicht, sondern ermöglicht ihn. Die historische Erfahrung zeigt, dass ein solcher schrittweise Wechsel die Gefahr von Fehlimplementierungen stark reduziert.
Domain‑Driven Design als Leitfaden
Die richtigen Grenzen für Module (und, falls nötig, Dienste) kommen aus der Domain, nicht aus technischen Schichten. DDD‑s Bounded Contexts bieten das konzeptuelle Werkzeug: Ein Bounded Context ist eine Grenze, innerhalb derer ein bestimmtes Domain‑Modell gültig und konsistent ist. Ein Bounded Context passt sich natürlich einem Modul an. Der Orders‑Kontext besitzt das Konzept einer Bestellung. Der Inventory‑Kontext besitzt das Konzept von Lagerbeständen. Sie kommunizieren über Domain‑Events, nicht über gemeinsame Objekte.
// Falsch: gemeinsames Domain‑Objekt überschreitet Modulgrenze
import { Order } from '../orders/internal/Order'; // ❌ greift in internal ein
// Richtig: über öffentliche Schnittstelle und Events kommunizieren
import { OrderPlacedEvent } from '../orders/OrderEvents'; // ✅ öffentlicher Vertrag
class InventoryModule {
@OnEvent('order.placed')
async handleOrderPlaced(event: OrderPlacedEvent): Promise<void> {
await this.reserveStock(event.orderId, event.items);
}
}
Wann Microservices die richtige Antwort sind
Microservices sind gerechtfertigt, wenn bestimmte Bedingungen erfüllt sind, nicht als Standard‑Architektur.
Unabhängige Skalierungsanforderungen. Wenn eine Komponente auf das 100‑fache der Last des restlichen Systems skalieren muss und diese Skalierung tatsächlich unabhängig ist, ergibt ein separater Dienst Sinn. Nur wenn die Last tatsächlich getrennt ist, spart man Ressourcen, weil man den stark ausgelasteten Dienst separat skalieren kann, ohne das gesamte System zu überdimensionieren. Ein Video‑Transkodierungs‑Pipeline, die 500 Worker benötigt, während der Rest der Anwendung nur 5 Worker braucht, ist ein typisches Beispiel.
Unabhängige Deployment‑Anforderungen. Wenn verschiedene Teile des Systems in unterschiedlichen Zeitplänen von unterschiedlichen Teams mit variierenden Risikotoleranzen deployed werden müssen, erzwingen Service‑Grenzen diese Unabhängigkeit. Dabei müssen automatisierte Pipelines und ein robustes On‑Call‑Modell vorhanden sein, sonst entstehen unkontrollierbare Release‑Risiken.
Technologische Heterogenität. Wenn eine Komponente tatsächlich eine andere Laufzeit, Sprache oder Infrastruktur benötigt (zum Beispiel ein Python‑ML‑Inference‑Dienst neben einer TypeScript‑API), ist eine Service‑Grenze die natürliche Lösung. Ein Python‑basiertes Machine‑Learning‑Modell, das GPU‑Beschleunigung benötigt, lässt sich nicht sinnvoll in eine Java‑Monolith‑Umgebung einbetten.
Organisatorische Größe. Conway’s Law besagt, dass Systeme die Kommunikationsstrukturen ihrer Erzeuger widerspiegeln. Wenn 20 Teams nicht effektiv koordinieren können, wird ein modularer Monolith zum Engpass. Microservices können jedem Team echte Autonomie geben, aber nur, wenn das Team groß genug ist, um den operationalen Overhead zu tragen. So entsteht eine effektivere Zusammenarbeit und eine bessere Anpassung an die Organisationsstruktur.
Der Migrationspfad
Der modulare Monolith ist auch ein nützlicher Zwischenschritt für Teams, die schließlich Dienste extrahieren möchten. Ein gut modularisierter Monolith macht die Extraktion unkompliziert: Die Modulgrenze ist bereits definiert, die Schnittstelle ist bereits explizit, und der Datenbesitz ist bereits klar.
Schritt 1: Modularer Monolith mit In‑Process‑Events
Orders‑Modul → [In‑Process‑Event‑Bus] → Inventory‑Modul
Schritt 2: Event‑Bus durch dauerhaften Broker ersetzen (keine Schnittstellenänderung)
Orders‑Modul → [Kafka‑Topic] → Inventory‑Modul
Schritt 3: Inventory in separaten Dienst extrahieren (keine Domain‑Änderung)
Orders‑Dienst → [Kafka‑Topic] → Inventory‑Dienst
Der umgekehrte Weg ist ebenfalls gültig. Mehrere hochkarätige Teams, darunter Shopify, Stack Overflow und Amazon Prime Video, haben öffentlich beschrieben, wie sie Microservices zurück in Monolithen oder modulare Monolithen konsolidiert haben, als der operationelle Overhead die Vorteile überwog. Diese Flexibilität ermöglicht es Teams, ihre Architektur flexibel an ihre Bedürfnisse anzupassen, während sie sich im Laufe der Zeit entwickeln. Es lässt sich ein einheitlicher Deployment‑Pipeline beibehalten und gleichzeitig asynchronen Nachrichtenaustausch testen.
Das ehrliche Entscheidungsrahmenwerk
| Frage | Microservices | Modularer Monolith |
|---|---|---|
| Teamgröße | Mehrere große Teams | Ein Team oder wenige kleine Teams |
| Deployment‑Häufigkeit | Dienste deployen unabhängig | Koordinierte Releases akzeptabel |
| Skalierungsanforderungen | Komponenten skalieren unabhängig | Einheitliche Skalierung ausreichend |
| Operationelle Reife | Starkes Platform‑Engineering | Begrenzte Platform‑Investition |
| Domain‑Klarheit | Grenzen sind gut verstanden | Domain wird noch entdeckt |
| Aktueller Zustand | Greenfield oder reife Domain | Frühphase oder sich entwickelnde Domain |
Der häufigste Fehler ist die Wahl von Microservices, bevor die Domain gut verstanden ist. Das Aufteilen eines Systems entlang falscher Grenzen ist teuer und riskant. Ein modularer Monolith ermöglicht es, die richtigen Grenzen zuerst zu entdecken und dann Dienste zu extrahieren, wenn die Grenzen bewiesen und die operationelle Investition gerechtfertigt ist. So vermeiden wir, dass wir später ganze Service‑Verträge neu verhandeln müssen.
Warum falsche Grenzen so teuer zu korrigieren sind
Wenn eine Service‑Grenze an der falschen Stelle gezogen wird, sind die Kosten nicht nur Refactoring. Jedes Team, das gegen diese Grenze gebaut hat, hat Code geschrieben, der sie voraussetzt. Jeder API‑Vertrag, jedes Event‑Schema, jede Datenbesitz‑Entscheidung baut auf der falschen Dekomposition auf. Das Rückgängigmachen bedeutet, Änderungen über mehrere Teams, mehrere Repositories und mehrere Deployment‑Pipelines gleichzeitig zu koordinieren, genau die Art von Koordination, die Microservices eliminieren sollten. In der Praxis bedeutet das, dass jede Änderung an einem API‑Contract sämtliche Verbraucher anpassen muss, was zu langen Release‑Zyklen führt.
Deshalb ist der modulare Monolith nicht nur eine einfachere Architektur, er ist eine Risikomanagement‑Strategie. Modulgrenzen in einem Monolithen sind günstig zu verschieben. Man refaktoriert den Code, führt die Tests aus und deployed einmal. Service‑Grenzen in einem verteilten System sind teuer zu verschieben. Man verhandelt API‑Änderungen, koordiniert Deployments, verwaltet Datenmigration und aktualisiert jeden Consumer. Die Kosten einer falschen Grenze sind in einem verteilten System eine Größenordnung höher als in einem Monolithen. Dies macht modulare Monolithen zu einer sichereren und kosteneffektiveren Option, insbesondere wenn die Domain noch weiter entwickelt wird.
Die Implikation: Der richtige Zeitpunkt, sich auf eine Service‑Grenze festzulegen, ist, wenn man genug Produktionserfahrung mit der Domain hat, um sicher zu sein, dass die Grenze korrekt ist, nicht am Anfang eines Projekts, wenn die Domain am wenigsten verstanden wird. Dies gewährleistet, dass die Architektur fundiert und weniger wahrscheinlich ist, in der Zukunft erhebliche Anpassungen zu erfordern.

Comments (0)
Please sign in to leave a comment