Microservices und Monolith
Ein großes Programm oder viele kleine? Was Microservices wirklich versprechen, was sie kosten, warum der Schnitt wichtiger ist als die Größe – und warum der modulare Monolith für die meisten Teams der bessere Anfang ist.
Ein Monolith ist eine Anwendung, die als Ganzes gebaut und ausgeliefert wird – ein Prozess, eine Datenbank. Microservices zerlegen sie in viele kleine Dienste, die jeweils einen fachlichen Bereich verantworten, ihre eigenen Daten besitzen, unabhängig ausgeliefert werden und über das Netz miteinander reden.
Der eigentliche Nutzen ist organisatorisch: Viele Teams können unabhängig voneinander arbeiten und ausliefern. Der Preis ist technisch: Aus Methodenaufrufen werden Netzwerkaufrufe, aus Transaktionen verteilte Abläufe, aus einem Prozess Dutzende, die überwacht werden müssen. Für kleine Teams überwiegt meist der Preis. Ein gut gegliederter modularer Monolith bietet viel vom Nutzen ohne die Kosten – und lässt sich später entlang seiner Module aufteilen.
Was ein Aufruf über das Netz kostet
Im Monolithen ruft der Bestellcode den Lagercode einfach auf – ein Methodenaufruf, ein paar Nanosekunden, und er schlägt nie fehl, weil das Netz hakt. Bei Microservices wird daraus ein HTTP- oder gRPC-Aufruf. Stell eine Aufrufkette zusammen:
Daraus folgt eine Grundregel: Dienste sollten so geschnitten sein, dass eine typische Anfrage wenige von ihnen braucht – idealerweise einen. Und lange synchrone Ketten löst man, wo es geht, durch Ereignisse auf.
Was man gewinnt, was man bezahlt
| Monolith | Microservices | |
|---|---|---|
| Ausliefern | alles zusammen; eine Änderung, ein Release | jeder Dienst einzeln, ohne die anderen |
| Skalieren | das Ganze mehrfach starten | nur die Teile, die Last haben |
| Teams | alle im selben Code, Abstimmung nötig | jedes Team besitzt seine Dienste |
| Fehler | ein Speicherleck reißt alles mit | Ausfälle bleiben eher lokal – wenn man sie abfängt |
| Konsistenz | eine Datenbank, echte Transaktionen | verteilte Daten, Sagas, Eventual Consistency |
| Fehlersuche | ein Stacktrace, ein Log | verteiltes Tracing über viele Dienste nötig |
| Betrieb | ein Prozess, eine Pipeline | Dutzende Pipelines, Container-Orchestrierung, Service-Netz |
| Umbauen | Refactoring mit der IDE über alles hinweg | Schnittstellen zwischen Teams verhandeln und versionieren |
Zwei Regeln trennen gute von schlechten Microservice-Architekturen. Erstens: Jeder Dienst besitzt seine Daten. Teilen sich zwei Dienste eine Datenbanktabelle, sind sie nicht unabhängig – eine Spaltenänderung bricht beide. Zweitens: Der Schnitt folgt der Fachlichkeit, nicht der Technik. Was das bedeutet, zeigt das nächste Beispiel.
Wo schneidet man?
Ein Online-Shop soll in drei Dienste zerlegt werden. Zwei Vorschläge liegen auf dem Tisch. Wähle einen – und dann eine typische Änderung, wie sie jede Woche vorkommt:
Der technische Schnitt erzeugt einen verteilten Monolithen: alle Nachteile verteilter Systeme, aber keiner der Vorteile – denn jede Änderung erfordert, dass alle Dienste gemeinsam geändert und ausgeliefert werden. Der fachliche Schnitt folgt dem, was im Domain-Driven Design Bounded Context heißt: ein Bereich, in dem Begriffe eine eindeutige Bedeutung haben. „Kunde“ bedeutet im Versand eine Lieferadresse, in der Buchhaltung eine Zahlungshistorie – das sind zwei verschiedene Modelle, und sie dürfen in zwei Diensten verschieden aussehen.
Conways Gesetz
Schon 1968 formulierte Melvin Conway eine Beobachtung, die heute ein Gesetz heißt: Organisationen bauen Systeme, deren Struktur ihre Kommunikationsstruktur abbildet. Drei Teams bauen drei Komponenten – ob die Fachlichkeit das hergibt oder nicht. Daraus folgt umgekehrt: Wer eine bestimmte Architektur will, muss die Teams passend zuschneiden. Microservices ohne Teams, die jeweils einen Dienst wirklich besitzen, liefern selten ihr Versprechen.
Der Mittelweg: modularer Monolith
Ein modularer Monolith wird als ein Programm ausgeliefert, ist innen aber in klar getrennte Module gegliedert – nach denselben fachlichen Grenzen, die man Microservices geben würde. Module greifen nicht in die Interna der anderen, kommunizieren über definierte Schnittstellen oder Ereignisse und besitzen ihre eigenen Tabellen. Man bekommt klare Struktur, unabhängiges Arbeiten im Code und eine spätere Aufteilung ohne große Umbauten – aber Methodenaufrufe, eine Transaktion und einen Prozess.
Viele bekannte Architekten raten deshalb: erst Monolith, dann aufteilen – wenn die fachlichen Grenzen sich bewährt haben und ein konkreter Grund vorliegt. Für den Weg dahin gibt es das Strangler-Fig-Muster, benannt nach einer Würgefeige: Neue Funktionen entstehen als eigener Dienst neben dem Monolithen, ein Proxy leitet Schritt für Schritt Anfragen um, bis der alte Teil abgeschaltet werden kann.
Für Entwickler übersetzt
// Spring Modulith: Pakete direkt unter der Hauptklasse sind Module –
// dieser Test schlägt fehl, sobald ein Modul in die Interna eines anderen greift
@Test
void modulgrenzenEingehalten() {
ApplicationModules.of(ShopApplication.class).verify();
}
// Module reden über Ereignisse statt über direkte Aufrufe
@Service
class BestellService {
private final ApplicationEventPublisher events;
@Transactional
void aufgeben(Bestellung b) {
repo.save(b);
events.publishEvent(new BestellungAufgegeben(b.id()));
}
}
// im Modul "lager" – läuft nach dem Commit, eigene Transaktion
@ApplicationModuleListener
void beiBestellung(BestellungAufgegeben e) { reservierung.reserviere(e.bestellId()); }| Werkzeug | Wofür |
|---|---|
| Spring Modulith | Module im Monolithen prüfen, dokumentieren, mit Ereignissen verbinden – und später leicht herauslösen |
| ArchUnit | Architekturregeln als Tests, auch ohne Spring |
| Java-Module (JPMS) | Grenzen auf Sprachebene: exports legt fest, was ein Modul nach außen zeigt |
| Vertragstests | Pact oder Spring Cloud Contract: prüfen, dass Dienste die Erwartungen ihrer Aufrufer erfüllen – ohne alles gemeinsam zu starten |
| OpenTelemetry | verteiltes Tracing – ohne das sind Fehler in Microservices kaum zu finden (siehe Observability) |