Der Monolith ist nicht der Feind
Die Microservices-Kool-Aid
Irgendwann um 2015 hat die Softwarebranche kollektiv beschlossen, dass Monolithen schlecht sind. Legacy. Technische Schulden in Personenform. Wenn Du Deine Anwendung nicht in Dutzende unabhängig deploybare Services aufteilst, die über HTTP und Message Queues kommunizieren, machst Du’s falsch.
Ein Jahrzehnt später schaue ich Unternehmen dabei zu, wie sie zurück zum Monolithen migrieren. Amazon hat seinen Prime-Video-Monitoring-Service von Microservices zu einem Monolithen umgebaut und die Kosten um 90% gesenkt. Shopify betreibt eine der größten E-Commerce-Plattformen der Welt auf einer monolithischen Rails-App. Basecamp ist seit Tag eins ein Monolith.
Was ist passiert? Die Realität ist passiert.
Die Microservices-Steuer
Microservices sind nicht umsonst. Sie kommen mit einer Steuer, die jedes Team zahlt — ob es das zugibt oder nicht:
Netzwerk ist nicht kostenlos
Wenn Dein Funktionsaufruf zu einem HTTP-Request wird, erbst Du eine ganze Kategorie von Problemen, die vorher nicht existierten:
- Latenz. Ein lokaler Funktionsaufruf dauert Nanosekunden. Ein Netzwerk-Call dauert Millisekunden — mindestens. Multipliziere das mit den 15 Services, die Dein Request durchläuft, und Du hast hunderte Millisekunden zu jeder Nutzerinteraktion addiert.
- Fehlermodi. Netzwerke fallen aus. Services gehen down. Timeouts passieren. Jetzt braucht jeder Inter-Service-Call Retries, Circuit Breaker, Fallbacks und Timeout-Konfiguration. Das ist eine Menge Code, der nichts mit Deiner Geschäftslogik zu tun hat.
- Serialisierungs-Overhead. JSON-Marshaling und -Unmarshaling an jeder Grenze. Protocol Buffers helfen, bringen aber eigene Komplexität mit.
# Ein "einfacher" Nutzerprofil-Request in Microservices-Land:
Client → API Gateway → Auth Service → User Service → Profile Service
→ Preferences Service
→ Avatar Service (→ CDN)
← Aggregierte Response
# Im Monolithen:
Client → App → Datenbank-Query → Response
# Gleiches Ergebnis. Eins hat 6 Netzwerk-Hops. Eins hat null.
Verteilte Systeme sind schwer
In dem Moment, wo Du Deine Anwendung über mehrere Services verteilst, baust Du ein verteiltes System. Das heißt:
- Verteilte Transaktionen. Dein Monolith hatte Datenbanktransaktionen. Jetzt brauchst Du Sagas, Kompensationslogik und Eventually-Consistent-State-Machines. Viel Spaß, das einem Junior-Entwickler zu erklären.
- Datenkonsistenz. Service A hat seine Datenbank aktualisiert. Der Cache von Service B ist veraltet. Der Nutzer sieht inkonsistente Daten. Das ist ein fundamentales Problem verteilter Systeme, und es gibt keine magische Lösung.
- Debug-Hölle. Ein Request schlägt fehl. Welcher der 12 beteiligten Services ist das Problem? Hoffentlich hast Du Distributed Tracing eingerichtet. Und hoffentlich funktioniert es. Und hoffentlich kannst Du das Waterfall-Diagramm mit 47 Spans lesen.
Operationelle Komplexität
Jeder Microservice braucht:
- Seine eigene CI/CD-Pipeline
- Seine eigene Deployment-Konfiguration
- Sein eigenes Monitoring und Alerting
- Sein eigenes Logging
- Seine eigenen Skalierungsregeln
- Seine eigene Datenbank (wenn Du’s “richtig” machst)
Fünf Microservices heißt fünfmal alles. Zwanzig Microservices heißt zwanzigmal alles. Und ein Platform-Team, das die Plattform verwaltet, die die Services verwaltet.
Ich habe 8-Personen-Teams gesehen, die 30+ Microservices betreiben. Die haben mehr Zeit mit Infrastruktur verbracht als mit Features. Das ist kein Engineering — das ist selbst zugefügter operationeller Schmerz.
Wann Monolithen funktionieren
Ein gut strukturierter Monolith ist eine vollkommen valide Architektur für die meisten Anwendungen. Hier ist, wann er die richtige Wahl ist:
Dein Team ist klein. Wenn Du weniger als 20 Entwickler hast, überwiegt der Koordinationsaufwand von Microservices wahrscheinlich die Vorteile. Ein Monolith lässt alle in der gleichen Codebasis arbeiten, zusammen deployen und zusammen debuggen.
Deine Domäne ist kohäsiv. Wenn Deine Services ständig gegenseitig aufrufen würden, hast Du keine unabhängigen Services — Du hast einen verteilten Monolithen. Das ist das Schlechteste aus beiden Welten.
Du verstehst Deine Domäne noch nicht vollständig. Microservice-Grenzen richtig zu ziehen ist extrem schwierig. Wenn Du zu früh splittest, wirst Du Jahre damit verbringen, Service-Grenzen zu refactoren. Ein Monolith lässt Dich schnell arbeiten und frei refactoren, bis Du verstehst, wo die echten Grenzen liegen.
Dein Datenmodell ist relational. Wenn Deine Services viele Daten teilen würden und Du sie entweder über Service-Datenbanken duplizieren oder ständig Cross-Service-Queries machen müsstest, ist eine einzelne Datenbank mit einem Monolithen simpler und konsistenter.
Der gut strukturierte Monolith
Das Schlüsselwort ist gut strukturiert. Ein Monolith muss kein Big Ball of Mud sein. Du kannst saubere Architektur innerhalb eines einzelnen Deployables haben:
/cmd
/server # Einstiegspunkt
/internal
/user # User-Domäne
/handler.go # HTTP-Handler
/service.go # Business-Logik
/repository.go # Datenzugriff
/billing # Billing-Domäne
/handler.go
/service.go
/repository.go
/notification # Notification-Domäne
/handler.go
/service.go
/repository.go
/pkg
/middleware # Geteilte Middleware
/database # Datenbank-Utilities
Jedes Domänen-Modul hat klare Grenzen. Abhängigkeiten fließen nach innen. Module kommunizieren über wohl definierte Interfaces. Du bekommst 80% der Modularitätsvorteile von Microservices ohne die Steuer verteilter Systeme.
Das nenne ich einen “modularen Monolithen”, und es ist die Architektur, die ich für die meisten Projekte empfehle, an denen wir arbeiten.
Wann tatsächlich splitten
Es gibt legitime Gründe, einen Service zu extrahieren. Aber sie sollten von echten Constraints getrieben sein, nicht von Architektur-Mode:
Unabhängige Skalierungsanforderungen
Deine Bildverarbeitung verbraucht 10x die CPU Deiner API? Das ist ein echter Grund zum Splitten. Du kannst den Image-Processor unabhängig skalieren, ohne Deine API-Server zu überprovisionieren.
Unterschiedliche Runtime-Anforderungen
Deine Haupt-App ist ein Go-HTTP-Server, aber Dein ML-Modell braucht Python mit GPU-Zugang? Verschiedene Deployment-Ziele sind ein valider Grund für separate Services.
Team-Autonomie im großen Maßstab
Du hast 100+ Entwickler und Teams treten sich gegenseitig auf die Füße mit widersprüchlichen Deployments? Service-Grenzen, die an Team-Grenzen ausgerichtet sind, ergeben Sinn — aber erst in dieser Größenordnung.
Fehler-Isolation
Ein Bug in Deiner Zahlungsverarbeitung sollte nicht Deinen Produktkatalog mitreißen. Wenn der Ausfall einer Komponente auf nicht verwandte Funktionalität übergreifen würde, bietet die Extraktion echten Mehrwert.
Compliance-Grenzen
PCI DSS verlangt, dass Deine Zahlungsverarbeitung in einer separaten, auditierten Umgebung läuft? Das ist eine harte Anforderung, keine Präferenz.
Das Entscheidungs-Framework
Bevor Du irgendetwas splittest, frag Dich:
- Welches konkrete Problem löst das? Wenn Du kein konkretes, aktuelles Problem benennen kannst, splitte nicht.
- Kann ich es innerhalb des Monolithen lösen? Bessere Modul-Grenzen, Background-Job-Queues und Read-Replicas lösen die meisten Skalierungsprobleme ohne Microservices.
- Bin ich auf die operationellen Kosten vorbereitet? Neue CI/CD-Pipeline, neues Monitoring, neues Deployment, neue On-Call-Rotation. Alles davon.
- Wird der Service wirklich unabhängig sein? Wenn er synchrone Calls zu drei anderen Services braucht, um einen Request zu bearbeiten, ist er nicht unabhängig. Er ist ein verteiltes Monolith-Stück.
Wenn Du alle vier Checks bestehst, extrahiere. Wenn nicht, behalte es im Monolithen und schau später nochmal.
Der pragmatische Weg
Hier ist, was ich den meisten Kunden tatsächlich empfehle:
- Starte mit einem modularen Monolithen. Saubere Domänen-Grenzen, klare Interfaces, ein Deployment.
- Identifiziere echte Schmerzpunkte über die Zeit. Nicht theoretische — tatsächlich messbare Probleme.
- Extrahiere chirurgisch. Ein Service nach dem anderen, wenn es einen klaren Nutzen gibt.
- Behalte den Monolithen als Kern. Der Großteil Deiner Anwendungslogik gehört wahrscheinlich zusammen. Lass sie zusammen.
Das ist nicht sexy. Es bringt Dir keine Speaking-Slots auf der KubeCon. Aber es funktioniert. Es liefert Features. Es lässt kleine Teams schnell arbeiten. Und es braucht kein Platform-Team, nur um den Laden am Laufen zu halten.
Der echte Feind
Der Monolith ist nicht der Feind. Schlecht strukturierter Code ist der Feind. Und schlecht strukturierten Code kannst Du in einem Monolithen oder über 50 Microservices verteilt schreiben. Beim Monolithen musst Du ihn wenigstens nur an einer Stelle debuggen.
Bau das Einfachste, das funktioniert. Splitte, wenn Du musst, nicht wenn Du kannst. Und lass niemals ein Architekturdiagramm auf einer Konferenz-Folie Deine Engineering-Entscheidungen diktieren.
Pragmatismus schlägt Dogma. Jedes Mal.