Monolith oder Microservices? Eine Architekturentscheidung
Bild: Ein Panda berät zwischen zwei Pfaden — genau wie du vor dieser Entscheidung.
Einleitung
Letzten Monat saß ich in einem Review mit einem Startup, das gerade seine zweite Finanzierungsrunde abgeschlossen hat. Ihr Produkt: eine Plattform für Lieferketten-Tracking. Ihr Plan: von Tag eins an Microservices, Kubernetes, Event-Driven Architecture. Fünfzehn Services, bevor das erste Mal ein Nutzer etwas bestellt hat.
Ich fragte nach der Teamgröße. Acht Entwickler. Nach der erwarteten Nutzerlast. “Vielleicht 10.000 Requests pro Tag, wenn es gut läuft.”
Wir haben den Architekturplan neu geschrieben. Als Monolith. Mit klaren Modulgrenzen. Und einer klaren Migrationstrategie, falls sie es irgendwann wirklich brauchen. Diese Geschichte ist kein Einzelfall — und sie zeigt, warum “Microservices first” einer der teuersten Irrtümer der letzten Jahre ist.
Kontext
Die Entscheidung zwischen Monolith und Microservices wird oft als rein technische Frage behandelt. Ist sie nicht. Sie ist eine Frage nach:
- Teamstruktur: Conway’s Law schlägt zu — die Architektur folgt der Organisation.
- Geschwindigkeit: Wie schnell muss ich ausliefern? Microservices haben Overhead.
- Betriebskosten: Monitoring, Logging, Netzwerk, Deployments — all das wird komplexer.
- Fehlerisolation: Ja, Microservices isolieren Fehler. Aber sie isolieren auch Datenbanktransaktionen.
Non-Goals: Ich sage nicht, dass Microservices generell schlecht sind. Ich sage, dass sie die falsche Standardantwort sind.
Option A: Monolith
Ein Deployment, eine Codebasis, eine Datenbank.
Pros:
- Einfaches Deployment:
docker compose upoder sogar ein Binary - Gemeinsame Datenbanktransaktionen — kein verteiltes Transaction-Problem
- Weniger Netzwerk-Latenz, weniger Serialisierungs-Overhead
- Anfangs schneller: weniger Infrastruktur, weniger Tooling
Cons:
- Wird mit der Zeit schwerer zu überschauen
- Skalierung nur vertikal oder mit viel Arbeit horizontal
- Technologie-Lock-in: alle Module nutzen den gleichen Stack
Option B: Microservices
Viele kleine, unabhängige Services, jeweils mit eigener Datenbank.
Pros:
- Unabhängige Skalierung: der Payment-Service skaliert anders als der Catalog-Service
- Technologie-Diversität: Rust für Performance-kritische Pfade, Go für Worker, Node.js für das Frontend-BFF
- Isolation: Ein Bug im Payment-Service crasht nicht den Catalog
Cons:
- Netzwerk-Latenz wird zum Feature: jeder Call ist ein potentieller Fehler
- Verteiltes Logging und Tracing werden zur Pflicht, nicht zur Option
- Datenbanken: wenn du relationale Konsistenz brauchst, wird es teuer
- Deployment-Pipeline: 15 Services = 15 CI/CD-Pipelines, 15 Helm-Charts, 15 Monitoring-Dashboards
Entscheidung
Fang mit dem Monolithen an. Aber baue ihn modular.
Die Architektur, die ich empfehle, sieht so aus: eine Codebasis, aber mit klaren Modulgrenzen und internen APIs. Wenn das Team wächst und ein Modul wirklich unabhängig werden muss, kannst du es extrahieren.
Der Grund? Die meisten Systeme erreichen nie die Größe, die Microservices rechtfertigt. Aber fast alle Systeme profitieren von sauberer Modularität. Das bekommst du auch im Monolithen — mit deutlich weniger operationalem Ballast.
Trade-offs, die ich akzeptiere:
- Gemeinsame Datenbank: ich nehme das Risiko des technischen Lock-ins in Kauf
- Vertikale Skalierung: für 99% der Use-Cases reicht ein größerer Server
- Spätere Migration: ich plane den Aufwand ein, aber ich realisiere ihn nicht prophylaktisch
Implementierung
Hier ein Beispiel, wie du Modulgrenzen im Monolithen erzwingst — mit einer einfachen Verzeichnisstruktur und Go-Internal-Packages:
services/
├── catalog/
│ └── internal/
│ ├── api.go # Öffentliche API des Moduls
│ └── store.go # Datenbankzugriff
├── payment/
│ └── internal/
│ ├── api.go
│ └── gateway.go
└── main.go # Komposition: initialisiert alle Module
Die internal/-Ordner verhindern, dass andere Module auf Implementierungsdetails zugreifen. Du bekommst damit eine Form von Compile-Time-Enforcement.
// main.go
package main
import (
"redpanda/internal/catalog"
"redpanda/internal/payment"
)
func main() {
catalogAPI := catalog.NewAPI(db)
paymentAPI := payment.NewGateway(cfg)
router.Handle("/catalog", catalogAPI.Routes())
router.Handle("/payment", paymentAPI.Routes())
}
Wenn der Payment-Service später auszieht, ersetzt du die In-Memory-Komposition durch einen HTTP-Client oder gRPC — und die Modulgrenze bleibt intakt.
Fazit
Die Architekturentscheidung ist selten eine Frage von “Monolith vs. Microservices”. Sie ist eine Frage von “Was brauche ich jetzt, und was kann ich später leicht ändern?”
Monolithen modular gebaut sind der sweet spot für die meisten Teams: schnelle Auslieferung, einfacher Betrieb, und trotzdem die Option zur Extraktion.
Meine Frage an dich: In welcher Architektur steckst du gerade fest, und was würdest du beim nächsten Mal anders machen?
Siehe auch: