Monorepo with Java: How to Organize Code for a Large Project
In modern Java project development, the question often arises: should you store all components in one repository (monorepo) or split them into several separate repositories (multirepo)? A monorepo is an approach where the code of multiple projects, libraries, and services is stored in a single version control system. In this article, we will explore how to effectively organize Java code in a monorepo using Maven and Gradle, and look at practical examples.
Advantages and Disadvantages of a Monorepo for Java
Before moving to implementation, it's important to understand when a monorepo is justified. For Java projects, it is especially useful if you have shared libraries, data models, or interfaces used by multiple services.
Pros of a Monorepo
- Single version of dependencies: all modules use the same library versions, eliminating conflicts.
- Simple refactoring: changing a shared API or data model happens atomically in a single commit.
- Simplified CI/CD: one pipeline for building and testing all modules.
- Transparency: every developer sees the entire project code, making it easier to understand the architecture.
Cons and How to Mitigate Them
- Repository size: it can become huge over time. Solution: use
.gitignoreand do not store binary artifacts. - CI configuration complexity: you need to be able to run only changed modules. Tools like Bazel or Gradle Build Cache help.
- Long code review: if all changes go through a single pull request. Solution: use branch policies and automatic triggers.
Monorepo Structure with Java: Practical Example
Let's consider a typical structure for a Java monorepo using Maven. Suppose we have three modules: core (shared logic), service-api (REST API), and service-worker (background processor).
my-java-monorepo/├── pom.xml # Parent POM├── core/│ ├── pom.xml│ └── src/│ └── main/java/com/example/core/│ └── Utils.java├── service-api/│ ├── pom.xml│ └── src/│ └── main/java/com/example/api/│ └── MainController.java├── service-worker/│ ├── pom.xml│ └── src/│ └── main/java/com/example/worker/│ └── WorkerTask.java└── .gitignoreParent POM (pom.xml)
In the project root, a parent POM is created that manages versions and lists the modules:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-java-monorepo</artifactId> <version>1.0.0</version> <packaging>pom</packaging>
<modules> <module>core</module> <module>service-api</module> <module>service-worker</module> </modules>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.0</version> <type>pom</type> <scope>import</scope>