Multi-Module Projects

As applications grow, keeping everything in a single pom.xml becomes unwieldy. Multi-module projects let you split a large codebase into logical pieces — shared libraries, web layers, and service modules — while still building them together with one command. This chapter explains the mechanics and walks you through creating a simple two-module project.

Prerequisites

  • Solid understanding of pom.xml structure
  • Familiarity with dependencies and the Maven lifecycle

Aggregation vs. Inheritance

Maven supports two ways to relate projects. They often appear together, but they solve different problems.

Aggregation

An aggregator (or parent) POM lists its child modules in a <modules> block. When you run a command from the aggregator directory, Maven builds every module in the declared order.

Aggregation answers: "Which projects should be built together?"

Inheritance

A parent POM defines shared configuration — properties, dependency versions, plugin settings — in one place. Child modules reference the parent with a <parent> block and inherit its defaults.

Inheritance answers: "What configuration should all these projects share?"

A single project can be both an aggregator and a parent. In practice, most multi-module setups combine both.

Project Structure

Here is the layout for a simple multi-module project with a shared library and an application that depends on it:

text
my-project/
  pom.xml          # Parent / aggregator POM
  common/
    pom.xml        # Shared utilities module
    src/
      main/
        java/
  app/
    pom.xml        # Application module
    src/
      main/
        java/

The Parent POM

Create my-project/pom.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
 
    <!-- POM model version -->
    <modelVersion>4.0.0</modelVersion>
 
    <!-- Parent coordinates -->
    <groupId>com.example</groupId>
    <artifactId>my-project</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <!-- This POM produces no artifact of its own -->
    <packaging>pom</packaging>
 
    <!-- Declare child modules -->
    <modules>
        <module>common</module>
        <module>app</module>
    </modules>
 
    <!-- Shared properties for all children -->
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.10.0</junit.version>
    </properties>
 
    <!-- Centralize dependency versions -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

Notice <packaging>pom</packaging>. This tells Maven that the parent project is not a library or application — it is a configuration container.

The Common Module

Create my-project/common/pom.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
 
    <!-- Reference the parent POM -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>my-project</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
 
    <!-- Child artifact coordinates -->
    <artifactId>common</artifactId>
 
    <!-- No version needed; inherited from parent -->
</project>

Add a utility class at my-project/common/src/main/java/com/example/common/StringUtils.java:

java
package com.example.common;
 
public class StringUtils {
 
    public static boolean isBlank(String str) {
        // Return true if the string is null or contains only whitespace
        return str == null || str.trim().isEmpty();
    }
}

The App Module

Create my-project/app/pom.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
 
    <!-- Reference the same parent -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>my-project</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
 
    <artifactId>app</artifactId>
 
    <dependencies>
        <!-- Depend on the sibling common module -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>common</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

Add the application entry point at my-project/app/src/main/java/com/example/app/Main.java:

java
package com.example.app;
 
import com.example.common.StringUtils;
 
public class Main {
 
    public static void main(String[] args) {
        // Use a method from the common module
        boolean blank = StringUtils.isBlank("  ");
 
        // Print the result
        System.out.println("Is blank: " + blank);
    }
}

Building the Multi-Module Project

Navigate to the parent directory and run:

bash
# Build all modules in dependency order
mvn clean install

Maven reads the parent pom.xml, discovers the modules, and builds them in the correct order. Because app depends on common, Maven builds common first.

The build output looks like this:

text
[INFO] Reactor Build Order:
[INFO] my-project [pom]
[INFO] common [jar]
[INFO] app [jar]
...
[INFO] BUILD SUCCESS

The "Reactor" is Maven's internal name for the collection of modules being built together. It analyzes inter-module dependencies and schedules the build so that no module is compiled before its dependencies.

Building a Single Module

If you are working on app and do not want to rebuild common, use the -pl (projects list) flag:

bash
# Build only the app module (assumes common is already installed)
mvn clean install -pl app

If common has changed and app needs the updated version, add -am (also-make) to include upstream dependencies:

bash
# Build app and any modules it depends on
mvn clean install -pl app -am

Version Management in the Parent

The <dependencyManagement> block in the parent POM is where multi-module projects shine. It lets you declare a version once and reference the dependency in any child module without repeating the version.

Child module with inherited version:

xml
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <!-- Version omitted; inherited from parent's dependencyManagement -->
        <scope>test</scope>
    </dependency>
</dependencies>

Tip

Version Properties

Define a property for every externally managed version in the parent POM. When a library releases a security patch, you update one line in the parent, and every child module picks it up automatically on the next build.

FAQ

Can a child module have a different parent?

Yes, but then it cannot inherit from your project parent. A POM can only have one <parent>. If you need both external parent configuration and your own shared settings, use composition: import the external BOM in <dependencyManagement> and keep your project as the parent.

What happens if two modules depend on each other?

Maven detects circular dependencies and fails the build with an error. You must refactor the code to break the cycle — typically by extracting the shared concepts into a third module that both original modules depend on.

Do I need to run mvn install on the parent before building children individually?

Yes, if you build a child module in isolation and it depends on sibling artifacts, those siblings must already be present in your local repository (~/.m2/repository). Running mvn install from the parent installs all modules. Alternatively, use -am to let Maven build the required siblings automatically.

Why does my IDE show errors in the child module even though mvn install succeeds?

Most likely the IDE has not imported the multi-module structure correctly. In IntelliJ IDEA, open the parent pom.xml (or the project root folder) rather than individual child directories. Then reload the Maven project so the IDE recognizes the parent-child relationships.

Should the parent POM be published to a repository?

For internal company projects, publishing the parent POM to a private repository (Nexus or Artifactory) ensures that every developer and CI server resolves the same configuration. For open-source projects, the parent is typically published to Maven Central alongside the child artifacts.