Einleitung: Warum ein Monorepository in Java-Projekten?
Wenn Java-Anwendungen an Komplexität zunehmen, stehen Teams oft vor einem Dilemma: den gesamten Code in einem Repository (Monorepository) speichern oder ihn auf viele Microservice-Repositories (Polyrepository) aufteilen. Die Praxis zeigt, dass ein Monorepository besonders effektiv für Projekte mit gemeinsamer Geschäftslogik, Bibliotheken und Konfigurationen ist. In Java ermöglicht es die zentrale Verwaltung von Abhängigkeiten, Versionen und Builds, was Refactoring und Integration vereinfacht.
In diesem Artikel werden wir Schritt für Schritt durchgehen, wie man Java-Code in einem Monorepository in der Praxis organisiert: von der Ordnerstruktur bis zur CI/CD-Einrichtung. Sie erfahren, wie Sie häufige Fehler vermeiden und die Entwicklung beschleunigen können.
1. Monorepository-Struktur für Java
Das Schlüsselprinzip eines Monorepositorys ist Modularität. In Java wird dies durch Multi-Modul-Projekte umgesetzt. Jedes Modul ist für seinen eigenen Bereich verantwortlich: gemeinsame Bibliotheken, Dienste, Hilfsprogramme.
Beispiel einer typischen Struktur:
my-java-monorepo/├── build.gradle # Root-Build-Datei├── settings.gradle # Enthält alle Module├── gradle.properties # Gemeinsame Abhängigkeitsversionen├── common-lib/ # Gemeinsame Bibliothek (DTOs, Hilfsprogramme)│ ├── build.gradle│ └── src/├── service-auth/ # Authentifizierungsdienst│ ├── build.gradle│ └── src/├── service-payment/ # Zahlungsdienst│ ├── build.gradle│ └── src/└── integration-tests/ # Integrationstests ├── build.gradle └── src/Für die Versionsverwaltung ist es praktisch, gemeinsame Abhängigkeiten in das Root-build.gradle oder pom.xml (für Maven) zu verschieben. In Gradle geschieht dies über den subprojects-Block:
subprojects { apply plugin: 'java' group = 'com.pythonlib' version = '1.0.0'
repositories { mavenCentral() }
dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' }}Diese Struktur erleichtert das Hinzufügen neuer Dienste ohne Kopieren von Konfigurationen und stellt sicher, dass alle Module dieselben Bibliotheksversionen verwenden.
2. Abhängigkeits- und Versionsverwaltung
Eines der Hauptprobleme eines Monorepositorys sind Versionskonflikte. In Java werden zur Lösung BOM (Bill of Materials) oder zentralisierte Abhängigkeitsbeschreibungen verwendet. Schauen wir uns ein Beispiel in Gradle mit java-platform an.
Erstellen wir ein platform-Modul, das alle externen Abhängigkeiten definiert:
// platform/build.gradleplugins { id 'java-platform'}
javaPlatform { allowDependencies()}
dependencies { constraints { api 'com.fasterxml.jackson.core:jackson-databind:2.15.2' api 'org.springframework.boot:spring-boot-starter-web:3.1.5' api 'org.slf4j:slf4j-api:2.0.9' }}Nun kann in jedem Dienst einfach auf die Plattform verwiesen werden:
// service-auth/build.gradledependencies { implementation platform(project(':platform')) implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-web'}Dies stellt sicher, dass alle Module dieselben Versionen verwenden. Wenn die Version in der platform aktualisiert wird, werden die Änderungen automatisch auf alle Dienste angewendet.
Tipp: Für Maven verwenden Sie <dependencyManagement> in der Root-pom.xml. Es funktioniert ähnlich.
3. CI/CD für ein Monorepository
Die Organisation von Continuous Integration in einem Monorepository erfordert einen besonderen Ansatz. Sie können nicht bei jedem Commit alle Module bauen — das würde die Pipeline verlangsamen. Die optimale Lösung sind inkrementelle Builds basierend auf Änderungen.
Beispiel auf GitHub Actions mit paths:
name: Java CIon: