Monorepository with Java: Code Organization in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why a Monorepository in Java Projects?

As Java applications grow in complexity, teams often face a dilemma: store all code in one repository (monorepository) or split it into many microservice repositories (polyrepository). Practice shows that a monorepository is especially effective for projects with shared business logic, libraries, and configurations. In Java, it allows centralized management of dependencies, versions, and builds, simplifying refactoring and integration.

In this article, we will break down how to organize Java code in a monorepository in practice: from folder structure to CI/CD setup. You will learn how to avoid common mistakes and speed up development.



1. Monorepository Structure for Java

The key principle of a monorepository is modularity. In Java, this is implemented through multi-module projects. Each module is responsible for its own area: shared libraries, services, utilities.

Example of a typical structure:

my-java-monorepo/├── build.gradle          # Root build file├── settings.gradle       # Includes all modules├── gradle.properties     # Shared dependency versions├── common-lib/           # Shared library (DTOs, utilities)│   ├── build.gradle│   └── src/├── service-auth/         # Authentication service│   ├── build.gradle│   └── src/├── service-payment/      # Payment service│   ├── build.gradle│   └── src/└── integration-tests/    # Integration tests    ├── build.gradle    └── src/

For version management, it is convenient to move shared dependencies to the root build.gradle or pom.xml (for Maven). In Gradle, this is done via the 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' }}

This structure makes it easy to add new services without copying configurations and ensures that all modules use the same library versions.



2. Dependency and Version Management

One of the main problems of a monorepository is version conflicts. In Java, BOM (Bill of Materials) or centralized dependency descriptions are used to solve this. Let's look at an example in Gradle using java-platform.

Let's create a platform module that defines all external dependencies:

// 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' }}

Now, in each service, you can simply reference the platform:

// service-auth/build.gradledependencies {    implementation platform(project(':platform'))    implementation 'com.fasterxml.jackson.core:jackson-databind'    implementation 'org.springframework.boot:spring-boot-starter-web'}

This ensures that all modules use the same versions. When the version in platform is updated, the changes are automatically applied to all services.

Tip: For Maven, use <dependencyManagement> in the root pom.xml. It works similarly.



3. CI/CD for a Monorepository

Organizing continuous integration in a monorepository requires a special approach. You cannot build all modules on every commit — this will slow down the pipeline. The optimal solution is incremental builds based on changes.

Example on GitHub Actions using paths:

name: Java CIon:                

Blogs

Book Recommendations