A Maven multi-module project is a parent POM with multiple child modules under it. The pattern is the standard for structuring Java applications larger than a single artifact. Done well, it provides clear module boundaries, centralized version management, and reusable configuration. Done badly, it produces tangled inheritance, inconsistent versions, and slow builds.
This page covers the patterns that work and the conventions to avoid.
my-project/
├── pom.xml (parent / aggregator)
├── my-project-api/
│ └── pom.xml (interfaces and types)
├── my-project-impl/
│ └── pom.xml (implementation)
├── my-project-server/
│ └── pom.xml (deployment artifact)
└── my-project-tests/
└── pom.xml (integration tests)
The parent POM has <packaging>pom</packaging> and declares the modules:
<modules>
<module>my-project-api</module>
<module>my-project-impl</module>
<module>my-project-server</module>
<module>my-project-tests</module>
</modules>
A well-designed parent POM:
<dependencyManagement><pluginManagement>It does NOT:
The distinction matters:
<!-- In parent: dependencyManagement -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>shared-lib</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- In child: just declare, version inherited -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>shared-lib</artifactId>
</dependency>
</dependencies>
dependencyManagement declares versions but does not add dependencies; child modules opt in by listing the dependency without a version. This pattern centralizes versions while allowing modules to choose which dependencies they actually need.
A BOM is a POM that exists only to declare dependency versions. Used for "import" the version map into other projects:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
This imports Spring Boot's BOM, which sets versions for hundreds of libraries Spring Boot is tested against. Your project then uses Spring Boot's versions automatically.
For your own project, a BOM module is the right pattern when external consumers need to depend on your project. They can import your BOM and get a tested set of versions.
A module depending on another module in the same multi-module project:
<dependency>
<groupId>com.example</groupId>
<artifactId>my-project-api</artifactId>
<version>${project.version}</version>
</dependency>
Use ${project.version} so the version is automatically the same as the parent. Avoid hard-coding versions in inter-module dependencies.
my-project-api/ <- interfaces, DTOs, exceptions
my-project-impl/ <- implementation classes
Other modules depend on my-project-api. The implementation is private and pluggable. Useful for libraries that expose a stable API.
A my-project-server (or -app, -deploy) module produces the deployable artifact (WAR, JAR with main class). Other modules are libraries.
This separation makes the deployment artifact clear; the server module's POM tends to be longer (Spring Boot config, packaging configuration) and it's good to isolate that.
A my-project-tests (or -it, -it-tests) module for integration tests that span the whole application. Keeps slow tests separate from unit tests in individual modules.
For multi-module projects, this is often a Maven Failsafe-driven module that runs against the deployed server module.
Multi-module builds can be slow at scale. Specific levers:
mvn clean install -T 1C
Builds modules in parallel. Speedup proportional to the number of independent modules.
Caveat: integration tests sharing fixed ports cannot run in parallel. See the JSPWiki/Wikantik convention of running integration tests serially.
-pl and -amBuild only specific modules:
mvn install -pl my-project-impl -am
-pl selects the module; -am ("also-make") includes its dependencies. Useful for fast iteration during development.
Maven's incremental support is limited. For real incremental needs, Gradle is better; or use -pl selectively.
mvn install -DskipTests
For iteration when you've already verified test results elsewhere.
Parent → child → grandchild → great-grandchild. Each level adds complexity for little benefit. Two levels (parent + module children) is plenty for most projects.
If you find yourself adding <dependency> to the parent that only one module needs, move it to the module. Parent should be common ground, not sprawl.
Versions belong in dependencyManagement (parent or BOM). Each module just declares which dependencies it uses.
<scope>provided</scope>For dependencies that are provided by the runtime (servlet API, Spring Boot's auto-configured beans), use <scope>provided</scope> to avoid bundling them.
<version>1.+</version> or LATEST produce non-reproducible builds. Always use specific versions.
dependencyManagement..idea/, .iml files, etc. Use .gitignore.