Mehrere Services, ein Frontend, gemeinsame Libraries – und überall dieselben Abhängigkeiten und Code-Standards: Genau hier wird ein Monorepo interessant. In der Praxis scheitert das Konzept selten am Git-Repo selbst, sondern an fehlenden Grenzen, zu breiten Build-Pipelines und einer unklaren Release-Strategie. Mit dem richtigen Zuschnitt entsteht dagegen ein Repository, das Änderungen nachvollziehbar hält, Wiederverwendung fördert und Team-Schnittstellen transparenter macht.
Wann ein gemeinsames Repository Vorteile bringt
Ein Monorepo ist vor allem dann sinnvoll, wenn viele Komponenten technisch zusammenhängen: ein Design-System, geteilte Domain-Modelle, interne SDKs oder mehrere Deployables, die sich häufig gemeinsam weiterentwickeln. Der größte Gewinn entsteht durch konsistente Standards (Linting, Test-Setup, Build-Toolchain) und durch Änderungen über Komponenten hinweg, die in einem Pull-Request mit klarer Historie landen.
Typische Situationen aus echten Projekten:
- Ein API-Contract ändert sich, und Client sowie Server müssen im selben Change angepasst werden.
- Eine Sicherheitslücke in einer gemeinsamen Library muss in mehreren Apps gleichzeitig behoben werden.
- Ein neues Logging- oder Observability-Format soll über alle Services einheitlich ausgerollt werden.
Wichtig ist die Gegenprobe: Wenn Teams praktisch unabhängig arbeiten, getrennte Release-Zyklen haben und kaum Code teilen, wird ein Multi-Repo oft ruhiger zu betreiben. Ein gemeinsames Repository ist kein Selbstzweck; es ist ein Organisations- und Tooling-Entscheid.
Struktur: klare Grenzen statt Ordner-Wildwuchs
Ohne klare Konventionen wird ein Repo schnell zur Ablage. Bewährt ist eine Top-Level-Struktur, die zwischen deploybaren Einheiten und wiederverwendbaren Bausteinen unterscheidet. Eine einfache, gut wartbare Aufteilung sieht häufig so aus:
- apps: Deployables wie Web-Frontend, Worker, API, CLI
- packages: Libraries/SDKs, UI-Komponenten, geteilte Tools
- infra: IaC, Helm-Charts, Docker-Assets (nur wenn das Team diese wirklich gemeinsam pflegt)
- docs: Architektur-Notizen, ADRs, Runbooks
Entscheidend sind Grenzen, die technisch überprüfbar sind. Statt „Bitte nicht quer importieren“ braucht es Regeln: Wer darf wen referenzieren? Welche Teile sind intern, welche stabil? Hier helfen Import-Regeln (z.B. per Linter), explizite Entry-Points pro Package und eine klare Public-API. Für Teams mit vielen Abhängigkeiten lohnt sich zusätzlich eine Abhängigkeits-Grafik, die zyklische Beziehungen sichtbar macht (und in CI blockiert).
Ownership sichtbar machen
Ohne Ownership wird jedes Refactoring politisch. Sinnvoll sind CODEOWNERS-Regeln pro App/Package: Reviews werden automatisch an die richtigen Personen geroutet. Zusätzlich sollte klar sein, welche Pakete „plattformnah“ sind (Build, CI, Basistools) und welche „produktorientiert“ sind. So landen Änderungen am Fundament nicht als Beifang in Feature-PRs.
Abhängigkeiten versionieren: intern ist nicht gleich beliebig
Ein verbreiteter Fehler: Interne Pakete werden wie „kostenlose Imports“ behandelt, weil sie im selben Repo liegen. Das führt zu stillen Breaking Changes. Sauberer ist eine der beiden Strategien:
- Semantic Versioning auch intern anwenden: Breaking Changes werden bewusst gemacht, Migrationen planbar.
- Oder bewusst „unversioniert“ arbeiten, aber dann mit strikten CI-Gates und Pflicht zu synchronen Updates in allen betroffenen Konsumenten.
Welche Variante passt, hängt vom Änderungsdruck und von der Anzahl der Konsumenten ab. Viele Konsumenten und mehrere Teams sprechen eher für echte Versionierung; wenige Konsumenten in einem Team können synchron mitziehen.
Builds und Tests: nur das ausführen, was sich geändert hat
Der Alltag entscheidet sich in der Pipeline: Wenn jeder Commit alles baut und testet, wird das Monorepo schnell als „langsam“ wahrgenommen. Das ist selten ein Naturgesetz, sondern meist fehlendes inkrementelles Denken. Ziel ist: betroffene Teile identifizieren, nur diese bauen/testen und dabei Ergebnisse wiederverwenden.
Inkrementelle Pipelines mit „affected“-Logik
In vielen Toolchains gibt es ein Konzept wie „affected projects“: Aus einem Git-Diff wird abgeleitet, welche Apps/Packages betroffen sind. Daraus ergeben sich gezielte Jobs: Lint nur für geänderte Pakete, Unit-Tests nur für betroffene Bereiche, E2E nur wenn das Frontend oder ein API-Contract verändert wurde. Das setzt voraus, dass Abhängigkeiten korrekt modelliert sind (z.B. Packages deklarieren, wen sie nutzen) und dass Tests sauber dem Scope zugeordnet sind.
Build Cache: Rechenzeit sparen, ohne Ergebnisse zu fälschen
Caching funktioniert nur, wenn Inputs eindeutig sind: Quellcode, Lockfile, Tool-Versionen, Umgebungsvariablen und Build-Flags. Fehlt ein Input in der Cache-Key-Berechnung, entstehen „grüne“ Builds mit falschen Artefakten. Eine solide Praxis ist:
- Lockfile und Toolchain-Versionen sind Teil des Cache-Keys.
- Builds laufen deterministisch (keine Zeitstempel im Output, stabile Reihenfolgen).
- Cache wird in CI zwischen Jobs geteilt, lokal optional.
Gerade bei großen Frontends oder TypeScript-Monorepos reduziert ein sauberer Cache die Feedback-Zeit erheblich. Wenn Builds „magisch“ wirken, ist das ein Signal für zu wenig Transparenz in den Cache-Inputs.
Test-Pyramide im Monorepo pragmatisch halten
Ein Monorepo verführt dazu, überall integrierte Tests zu schreiben, weil alles „nah“ beieinander liegt. Das skaliert schlecht. Besser ist eine klare Aufteilung:
- Unit-Tests pro Package, schnell und isoliert.
- Integrations-Tests pro App/Service (z.B. Datenbankadapter, HTTP-Controller).
- E2E-Tests gezielt für kritische Flows, nicht als Ersatz für Unit-Tests.
Für API-nahe Änderungen passt ergänzend Contract Testing, wenn Provider und Consumer unabhängig deployt werden. Das verhindert, dass ein „gemeinsames Repo“ fälschlich als „gemeinsame Release-Klammer“ verstanden wird.
Releases: pro Paket, pro App oder alles gemeinsam?
Die Release-Strategie entscheidet, ob ein Monorepo im Betrieb ruhig bleibt. Es gibt drei gängige Modelle, die sich kombinieren lassen:
- Single Version: Das gesamte Repo hat eine Version. Einfach, aber nur sinnvoll, wenn alles wirklich gemeinsam released wird.
- Independent Versions: Jedes Package/App hat eigene Versionen. Flexibel, aber braucht gutes Tooling.
- Release Trains: Regelmäßige, gebündelte Releases für bestimmte Bereiche (z.B. Plattform-Libs wöchentlich), während Apps bei Bedarf deployen.
Release Automation ohne manuelle Copy-Paste-Workflows
Unabhängig vom Modell sollte ein Release reproduzierbar sein: Changelog, Version-Bump, Tagging, Build-Artefakte und Publishing. Manuelle Schritte führen zu „Version drift“ (Code und veröffentlichte Version passen nicht mehr zusammen). In der Praxis bewähren sich:
- Conventional Commits oder zumindest strukturierte PR-Titel, damit Changelogs automatisch entstehen.
- Ein Release-Job in CI, der nur auf einem geschützten Branch läuft.
- Gating über Tests und Policy-Checks, bevor ein Tag entsteht.
Wenn Services als Container ausgeliefert werden, sollte die Image-Version an Git-Tags oder Commit-SHAs gebunden sein. Für Libraries gilt: veröffentlichte Artefakte müssen aus genau dem Commit gebaut werden, der getaggt wurde.
Breaking Changes sichtbar machen
In einem Monorepo passieren Breaking Changes häufig „aus Versehen“, weil Anpassungen direkt im gleichen PR mitgezogen werden. Für den Betrieb sind sie trotzdem Breaking Changes, wenn externe Nutzer oder andere Teams betroffen sind. Bewährt sind harte Signale:
- Explizite Major-Version-Bumps für öffentliche Pakete.
- Deprecation-Phasen mit Übergangs-API (z.B. alte Methode bleibt für eine Version erhalten).
- Automatisierte Migrationsskripte (Codemods) bei großen API-Umstellungen.
CI/CD für Monorepos: Policies, die Teams entlasten
Die Pipeline sollte Konflikte früh und automatisch lösen: Linting, Typechecks, Tests, Security-Scans. Wichtig ist, dass sie nicht „alles immer“ ausführt, sondern nach Scope skaliert. Zusätzlich helfen Policies, die Review-Arbeit planbar machen.
Pull-Request-Regeln mit technischer Begründung
Ein paar Regeln, die sich in großen Repos bewährt haben:
- Pflicht zu kleinen PRs: leichter reviewbar, weniger Merge-Konflikte.
- Keine „Drive-by“-Refactorings in Feature-PRs: Refactoring separat, mit eigener Testabsicherung.
- Tests müssen das betroffene Verhalten abdecken; Snapshot-Spam wird abgelehnt.
Bei Backend-Änderungen rund um Schnittstellen lohnt eine frühe Validation: Schema-Validation für APIs stoppt fehlerhafte Payloads, bevor sie sich über mehrere Apps ausbreiten.
Deployments entkoppeln, obwohl der Code zusammenliegt
Ein Monorepo bedeutet nicht, dass alles gemeinsam deployt werden muss. Sauber ist ein „Build once, deploy many“-Ansatz pro App: das Artefakt (Image, Bundle, Binary) entsteht eindeutig aus einem Commit und wird dann environmentspezifisch ausgerollt. Für Services mit unterschiedlichen Lastprofilen bleiben Skalierung und Rollout unabhängig.
Bei vielen Services ist außerdem wichtig, dass Retries und Nebenläufigkeit robust sind, weil mehr Komponenten gleichzeitig Änderungen bekommen. Für Hintergründe und Job-Worker passt Async Jobs im Backend als Ergänzung für stabile Verarbeitung.
Konkrete Schritte für den Start in einem bestehenden Projekt
- Repo-Zuschnitt festlegen: Welche Deployables in apps, welche Libraries in packages?
- Ownership einführen: CODEOWNERS pro Bereich, Review-Pflichten definieren.
- Abhängigkeitsregeln technisch erzwingen (Lint/CI), zyklische Abhängigkeiten blockieren.
- CI auf „affected“-Logik umstellen: nur betroffene Tests/Builds laufen lassen.
- Build Cache deterministisch machen: Inputs definieren, Cache-Key prüfen, Flakes eliminieren.
- Release-Modell entscheiden und automatisieren: Tags, Changelogs, Artefakte reproduzierbar.
- Dokumentation minimal, aber präzise: Public-APIs, Deprecations, Migrationspfade.
Häufige Stolperfallen und wie sie sich vermeiden lassen
„Alles ist shared“ – und niemand fühlt sich zuständig
Ein gemeinsames Repo darf nicht zu gemeinsamem Verantwortungsnebel führen. Plattformnahe Pakete brauchen klare Maintainer und stabile Schnittstellen. Sonst werden Breaking Changes im Nachhinein repariert und verlagern Aufwand in Reviews und Hotfixes.
Tooling-Schulden: zu viele Einzellösungen pro Team
Wenn jedes Team eigene Scripts, eigene Lint-Regeln und eigene Test-Runner einführt, entsteht im Monorepo ein Flickenteppich. Sinnvoll ist ein Kern an Standards (Formatierung, Linting, Test-Konventionen) und bewusst definierte Ausnahmen. Je mehr Automatisierung existiert, desto wichtiger ist Konsistenz, damit CI-Fehler eindeutig sind.
Performance-Probleme werden ignoriert, bis die Pipeline blockiert
Ein langsamer CI-Lauf ist nicht nur „unangenehm“, sondern verändert Verhalten: Entwickler umgehen Tests, bündeln zu große PRs oder verschieben Refactorings. Frühzeitige Messung hilft: Welche Jobs sind teuer? Wo entstehen die meisten Cache-Misses? Welche Tests flaken? Das sind konkrete Engineering-Aufgaben, keine Nebensache.
Security und Zugriffskontrolle werden zu spät gedacht
Ein Monorepo kann die Angriffsfläche im SDLC erhöhen, wenn Secrets, CI-Permissions oder Publish-Rechte zu breit sind. Bewährt sind minimal benötigte Rechte pro Pipeline-Job, getrennte Tokens für Publishing und signierte Artefakte, wo verfügbar. Für APIs sollte außerdem klar geregelt sein, wie Authentifizierung umgesetzt wird; als Einstieg passt JWT-Auth im Backend für typische Service-zu-Service- oder Client-API-Szenarien.
Entscheidungshilfe: Passt ein Monorepo zum eigenen Setup?
- Wenn häufig gemeinsame Änderungen an Client, Server und Libraries passieren:
- Monorepo ist meist sinnvoll, wenn Releases sauber entkoppelt bleiben.
- Wenn Teams unabhängig liefern und kaum Code teilen:
- Multi-Repo kann einfacher bleiben; Shared Libraries dann als eigene Pakete veröffentlichen.
- Wenn CI bereits heute langsam oder instabil ist:
- Erst Pipeline-Stabilität und Caching lösen, dann Repository-Strategie ausrollen.
- Wenn klare Ownership nicht etabliert ist:
- Zuerst Zuständigkeiten und Review-Regeln definieren, sonst skaliert die Reibung.
