Monorepo und Microservices – warum das kein Widerspruch ist

Repository-Schnitt und Deployment-Schnitt sind zwei verschiedene Fragen. Warum ein Dutzend Microservices in einem Monorepo für viele Teams der pragmatischste Weg ist – mit ehrlicher Bilanz.

Architektur

Monorepo und Microservices – warum das kein Widerspruch ist

Marcel Strahl7 Min. Lesezeit

Monorepo und Microservices – warum das kein Widerspruch ist

Liest man Blogartikel oder scrollt durch die einschlägigen Entwickler-Communities, bekommt man schnell den Eindruck, es gäbe genau zwei Lager: die Monolith-Fraktion, die alles in ein Repository packt und Microservices für überbewerteten Hype hält – und die Microservice-Fraktion, für die zu jedem Service selbstverständlich ein eigenes Repository, eine eigene Pipeline und am besten noch ein eigenes Team gehört. Wer beides mischt, macht angeblich beides falsch.

Meiner Meinung nach ist das Unsinn. Ich arbeite seit längerer Zeit an einem verteilten System, das aus über einem Dutzend Microservices besteht – und alle leben in einem einzigen Monorepo. Und ich behaupte: Genau diese Kombination ist für viele Teams der pragmatischste Weg. In diesem Artikel geht es bewusst nicht um eine bestimmte Programmiersprache oder ein Framework, sondern um das, was wirklich zählt: Architektur, Aufbau, Verständlichkeit – und eine ehrliche Abwägung der Vor- und Nachteile.

Warum überhaupt Microservices?

Bevor wir über das Repository reden, die unbequeme Frage zuerst: Braucht man überhaupt Microservices? In vielen Fällen lautet die ehrliche Antwort: nein. Ein sauber geschnittener Modulith deckt einen Großteil aller Anwendungsfälle ab, ohne dass man sich verteilte Systeme, Message-Broker und Eventual Consistency ins Haus holt.

Microservices lohnen sich dann, wenn ein Workflow aus Schritten besteht, die unterschiedlich skalieren, unabhängig fehlschlagen und unabhängig wiederholt werden müssen. Ein typischer Verarbeitungsprozess sieht zum Beispiel so aus:

Annahme ──► Validierung ──► Datenverarbeitung ──► Übergabe an Drittsystem
             (schnell &      (rechenintensiv       (gelegentlich
              günstig)        & langsam)            nicht erreichbar)

Die drei Schritte haben völlig unterschiedliche Anforderungsprofile: Die Validierung ist schnell und billig, die Datenverarbeitung frisst CPU, und das Drittsystem fällt gerne mal aus. Wenn jeder dieser Schritte ein eigener Service ist, kann der langsame Schritt unabhängig skaliert werden – und ein Ausfall des Drittsystems blockiert nicht die Annahme neuer Aufträge. Die Nachrichten stauen sich einfach in einer Queue, bis das System wieder da ist.

Das ist der Grund für Microservices. Nicht, welche Architektur auf Konferenzen gerade gehyped wird.

Monorepo heißt nicht Monolith

Die verbreitete Annahme, Microservices bräuchten zwingend ein Repository pro Service, hält einer Prüfung nicht stand. Repository-Schritt und Deployment-Schritt sind zwei völlig verschiedene Dinge. Das eine beantwortet die Frage „Wo liegt der Code?", das andere „Was wird unabhängig ausgeliefert?". Man kann sie unabhängig voneinander entscheiden.

Unser Aufbau sieht vereinfacht so aus:

├── lib/          # geteilte Bibliotheken (Contracts, Basis-Komponenten)
├── services/     # die eigentlichen Microservices, je einer pro Ordner
├── deployment/   # geteilte Infrastruktur-Konfiguration
└── docs/         # Architektur-Dokumentation

Jeder Service unter services/ ist eine vollwertige, eigenständige Anwendung mit eigenen Abhängigkeiten, eigenem Container-Image und eigenem, unabhängigem Deployment. Die geteilten Bibliotheken werden nicht über einen internen Paket-Server bezogen, sondern direkt per Pfad aus dem Repository eingebunden:

Ein Repository

├── Geteilte Bibliotheken
│     • Message-Contracts   ◄── Service A, B, C
│     • Basis-Komponenten   ◄── Service A, C

└── Unabhängig deploybare Services
      • Service A
      • Service B
      • Service C

Das klingt banal, ist aber der eigentliche Gamechanger: Ändert man eine geteilte Bibliothek, sieht man im selben Pull Request, welche Services betroffen sind – und passt sie direkt mit an. Kein Versions-Bumping über fünf Repositories, kein „Service X läuft noch auf Version 2.3 der Bibliothek und keiner weiß warum". Eine atomare Änderung über Service-Grenzen hinweg ist ein einziger Commit, ein einziges Review.

Der Vertrag zwischen den Services lebt im Code, nicht im Wiki

Unsere Services kommunizieren asynchron über einen zentralen Message-Bus. Es gibt genau zwei Arten von Nachrichten:

  • Commands – „Tu etwas": ein Auftrag an genau einen Service.
  • Events – „Etwas ist passiert": das Ergebnis, als Success oder Failed.

Jeder Service folgt demselben, bewusst langweiligen Muster: Command rein, Arbeit erledigen, immer genau ein Success- oder Failed-Event raus.

Orchestrator              Message-Bus              Fach-Service
     │                         │                        │
     │  Command: Berechne X    │                        │
     ├────────────────────────►│  Command: Berechne X   │
     │                         ├───────────────────────►│
     │                         │                        │ verarbeitet
     │                         │  Event: X berechnet    │ den Auftrag
     │  Event: X berechnet     │◄───────────────────────┤
     │◄────────────────────────┤  (Success)             │
     │                         │                        │
     │  nächster Command       │                        │
     ├────────────────────────►│                        │

Das klassische Problem verteilter Systeme ist dabei nicht die Technik, sondern der Vertrag: Wer definiert eigentlich verbindlich, wie eine Nachricht aussieht? In Multi-Repo-Setups endet das erfahrungsgemäß in einer Wiki-Seite, die seit acht Monaten nicht mehr stimmt – und der Breaking Change fällt nachts um drei in Produktion auf.

Im Monorepo liegen alle Nachrichten-Definitionen des Gesamtsystems in einer einzigen geteilten Bibliothek. Produzent und Konsument verwenden buchstäblich dieselbe Definition. Der Vertrag ist damit typisiert, wird von Werkzeugen geprüft und bricht sichtbar in der Pipeline – bevor irgendetwas deployed wird. Dazu kommt eine simple Namenskonvention (Command.<Kontext>.<Aktion>, Event.<Kontext>.<Aktion>.Success|Failed), die dafür sorgt, dass man nach zwei Minuten in jedem Service weiß, wo man suchen muss. Langweilig? Absolut. Genau das ist der Punkt.

Orchestrierung statt Event-Spaghetti

Ein häufiges und berechtigtes Argument gegen ereignisgetriebene Systeme: Irgendwann weiß niemand mehr, wer auf was reagiert. Das passiert, wenn man reine Choreographie betreibt – jeder Service lauscht still auf Events anderer Services, und der Gesamtprozess existiert nirgendwo, außer als Stammeswissen in den Köpfen.

Die Alternative ist Orchestrierung: Ein dedizierter Koordinator kennt den gesamten Workflow, lauscht auf alle Success- und Failed-Events und entscheidet, welcher Command als nächstes dispatched wird.

Choreographie – jeder kennt jeden:
 
  Service A ──► Service B ──► Service D ──► Service B …
      │                           ▲
      └─────► Service C ──────────┘
 
Orchestrierung – einer kennt den Prozess:
 
                     ┌──► Service A
                     ├──► Service B
  Orchestrator ◄─────┤
                     ├──► Service C
                     └──► Service D

Die fachlichen Services bleiben dadurch dumm im besten Sinne: Sie kennen weder ihren Vorgänger noch ihren Nachfolger. Der gesamte Prozess ist an genau einer Stelle ablesbar – und nur an dieser einen Stelle muss man ihn ändern, wenn sich der Ablauf ändert.

Der Orchestrator kann auch komplexere Muster abbilden, etwa mehrere parallele Prüfungen, die alle erfolgreich sein müssen, bevor es weitergeht:

                  ┌──► Prüfung 1 ──┐
                  │                │      alle drei    ja  ──► nächster Schritt
Orchestrator ─────┼──► Prüfung 2 ──┼──►  erfolgreich?
                  │                │                  nein ──► Fehlerbehandlung
                  └──► Prüfung 3 ──┘

Und weil in einem asynchronen System immer etwas schiefgeht: Eine Middleware protokolliert jede Nachricht in ein Message-Log. Schlägt ein Schritt fehl, lässt sich die Nachricht gezielt erneut einspielen – statt dass jemand von Hand Datenbank-Einträge frisiert. Nachvollziehbarkeit ist in verteilten Systemen kein Nice-to-have, sondern die halbe Miete.

Vor- und Nachteile im Überblick

Damit das hier keine Werbeveranstaltung wird, die ehrliche Bilanz:

VorteileNachteile
VerständnisDas Gesamtsystem ist an einem Ort erkundbar; der Workflow steht an genau einer StelleDas Repository wirkt auf Neulinge zunächst groß und einschüchternd
ÄnderungenAtomare Änderungen über Service-Grenzen in einem Pull RequestVerführt dazu, Grenzen zu ignorieren und Services „mal eben" mitzuändern
VerträgeContracts als Code, geteilt und werkzeuggeprüftGeteilte Bibliotheken werden zum Kopplungs-Magneten, wenn man nicht aufpasst
ToolingEin Setup, ein Task-Runner, eine Qualitäts-Pipeline für allesCI muss lernen, nur Betroffenes zu bauen – das ist echte Ingenieursarbeit
OnboardingEin Klon, ein Befehl, das ganze System läuft lokalDie lokale Umgebung muss ein verteiltes System emulieren können
BetriebServices skalieren, fallen aus und deployen unabhängigEventual Consistency, Idempotenz, Retries – die Komplexität verteilter Systeme bleibt voll erhalten

Drei dieser Punkte verdienen einen zweiten Blick, weil sie in der Praxis am meisten wehtun:

Die Grenze zwischen den Services wird weich. Wenn geteilter Code nur einen Import entfernt ist, wandert gerne mal zu viel in die gemeinsamen Bibliotheken. Eine geteilte Bibliothek für Nachrichten-Verträge ist ein Feature; eine geteilte Bibliothek, in der plötzlich Businesslogik von drei Services liegt, ist ein verteilter Monolith mit Extraschritten. Das muss im Review aktiv verteidigt werden. Das Monorepo macht Kopplung billig – genau deshalb muss man sie bewusst teuer halten.

Deployment-Disziplin bleibt Pflicht. Ein Repository verführt zu der Annahme, man könne alles gleichzeitig ausliefern. Kann man nicht – es sind weiterhin unabhängige Services, und Nachrichten von gestern liegen unter Umständen noch in einer Queue. Verträge müssen abwärtskompatibel weiterentwickelt werden, Monorepo hin oder her.

Asynchronität ist und bleibt der Preis. Nichts an der Komplexität verteilter Systeme verschwindet durch das Repository-Layout. Wer Idempotenz, Retries und Dead-Letter-Queues im Team nicht beherrscht, sollte sie sich nicht freiwillig einladen – egal wie die Repos geschnitten sind.

Fazit

Die Frage „Monorepo oder Microservices?" ist falsch gestellt – das eine ist eine Frage der Code-Organisation, das andere eine Frage des Deployments. Man kann sie unabhängig voneinander beantworten, und die Kombination aus beidem hat sich für uns als erstaunlich unaufgeregt herausgestellt.

Was am Ende zählt:

  • Microservices nur bei echtem Bedarf: unterschiedliche Skalierung, unabhängige Fehlerbehandlung, entkoppelte Deployments. Sonst: Modulith.
  • Monorepo für die Entwicklungsgeschwindigkeit: atomare Änderungen über Service-Grenzen, ein Tooling, ein Onboarding.
  • Verträge als Code in einer geteilten Bibliothek – typisiert und werkzeuggeprüft statt als Wiki-Fiktion.
  • Orchestrierung statt Choreographie, wenn der Workflow komplex wird – der Prozess gehört an eine Stelle, nicht in zwanzig Köpfe.
  • Disziplin bei den Grenzen: Das Monorepo macht Kopplung billig – genau deshalb muss man sie bewusst teuer halten.

Am Ende brauchen weder das Team noch das Unternehmen die architektonisch reinste Lösung. Sie brauchen: stabile Software, schnelle Entwicklung, wartbaren Code. Und dafür darf man Dinge auch mal kombinieren, die laut Internet nicht zusammengehören.

  • 5 Min. LesezeitPHP

    PHP-Grundlagen Teil 2 – Typen in PHP verstehen

    Welche Datentypen es in PHP gibt, wofür man sie wirklich braucht und warum strict_types von Anfang an gesetzt gehört – mit Übungsaufgaben zum Selbermachen.

    • #PHP
    • #Grundlagen
    • #Tutorial
    • #Software-Entwicklung
    • #Webentwicklung
    • #Backend
  • 5 Min. LesezeitC#

    C#-Grundlagen Teil 2 – Typen in C# verstehen

    Value Types, Reference Types, Nullable Reference Types und warum decimal für Geld gemacht ist: die Grundlagen des C#-Typsystems – mit Übungsaufgaben zum Selbermachen.

    • #C#
    • #Grundlagen
    • #Tutorial
    • #Software-Entwicklung
    • #Backend
  • 5 Min. LesezeitJava

    Java-Grundlagen Teil 2 – Typen in Java verstehen

    Primitives, Wrapper-Klassen, Autoboxing und die ==-Falle bei Strings: Was man über Javas Typsystem von Anfang an wissen sollte – mit Übungsaufgaben zum Selbermachen.

    • #Java
    • #Grundlagen
    • #Tutorial
    • #Software-Entwicklung
    • #Backend