Monorepository mit Java: Code-Organisation in der Praxis
\nIn der modernen Java-Entwicklung stellt sich immer häufiger die Frage der Wahl zwischen einem Monorepository (Monorepo) und mehreren einzelnen Repositories. Ein Monorepository ist ein Ansatz, bei dem der gesamte Code eines Projekts (mehrere Dienste, Bibliotheken, Konfigurationen) in einem einzigen Git-Repository gespeichert wird. In diesem Artikel werden wir uns ansehen, wie man ein Monorepository mit Java in der Praxis organisiert: von der Ordnerstruktur bis zur Einrichtung von Build und CI/CD. Sie erhalten fertige Codebeispiele und Empfehlungen, die Ihnen helfen, typische Fehler zu vermeiden.
\n\nWarum ein Monorepository für Java?
\nEin Monorepository ist besonders nützlich für Java-Projekte, bei denen viele Module voneinander abhängen. Die wichtigsten Vorteile:
\n- \n
- Einheitliche Abhängigkeitsversion – alle Module verwenden dieselbe Version von Bibliotheken, was Konflikte ausschließt. \n
- Vereinfachtes Refactoring – eine Änderung der API in einem Modul ist sofort in allen abhängigen Modulen sichtbar. \n
- Gemeinsame Build-Konfiguration – Gradle oder Maven werden einmal für alle Module konfiguriert. \n
- Integrität von CI/CD – eine Pipeline für alle Dienste, Änderungen sind leichter nachvollziehbar. \n
Allerdings erfordert ein Monorepository Disziplin: Ohne eine klare Struktur wird es chaotisch. Betrachten wir die richtige Organisation.
\n\nStruktur eines Monorepositorys für ein Java-Projekt
\nEmpfohlene Struktur für ein Monorepository mit Java (am Beispiel von Gradle):
\nmy-monorepo/\n├── build.gradle.kts # Root-Build-Datei\n├── settings.gradle.kts # Bindet alle Module ein\n├── gradle.properties # Gemeinsame Eigenschaften\n├── libs/ # Gemeinsame Bibliotheken\n│ ├── common-utils/\n│ │ ├── build.gradle.kts\n│ │ └── src/\n│ └── logging/\n│ ├── build.gradle.kts\n│ └── src/\n├── services/ # Microservices\n│ ├── user-service/\n│ │ ├── build.gradle.kts\n│ │ └── src/\n│ └── payment-service/\n│ ├── build.gradle.kts\n│ └── src/\n├── config/ # Konfigurationen (z. B. checkstyle, spotbugs)\n│ └── checkstyle.xml\n└── docker/ # Dockerfile für jeden Dienst\n ├── user-service.Dockerfile\n └── payment-service.Dockerfile\n\nWichtige Prinzipien:
\n- \n
- Module werden in
libs(Bibliotheken) undservices(ausführbare Anwendungen) unterteilt. \n - Das root-
build.gradle.ktsdefiniert gemeinsame Plugins und Versionen. \n - Die Datei
settings.gradle.ktsbindet alle Unterprojekte ein. \n
Beispiel für settings.gradle.kts:
rootProject.name = "my-monorepo"\n\ninclude(\n "libs:common-utils",\n "libs:logging",\n "services:user-service",\n "services:payment-service"\n)\n\n\nEinrichtung von Abhängigkeiten zwischen Modulen
\nDie Hauptaufgabe eines Monorepositorys ist die Verwaltung interner Abhängigkeiten. Betrachten wir ein Beispiel, bei dem user-service von common-utils abhängt.
Schritt 1. In der root-gradle.properties deklarieren wir Versionen:
# gradle.properties\nkotlinVersion=1.9.0\nspringBootVersion=3.2.0\ncommonUtilsVersion=1.0.0\n\nSchritt 2. In der root-build.gradle.kts binden wir Plugins ein und legen gemeinsame Einstellungen fest:
plugins {\n id("java-library") apply false\n id("org.springframework.boot") version "3.2.0" apply false\n id("io.spring.dependency-management") version "1.1.4" apply false\n}\n\nallprojects {\n group = "com.example"\n version = "1.0.0"\n\n repositories {\n mavenCentral()\n }\n}\n\nsubprojects {\n apply(plugin = "java-library")\n\n java {\n sourceCompatibility = JavaVersion.VERSION_17\n targetCompatibility = JavaVersion.VERSION_17\n }\n\n dependencies {\n implementation("org.slf4j:slf4j-api:2.0.9")\n testImplementation("org.j