Skip to content

Multimodule with Maven 4

Martin Desruisseaux edited this page Dec 1, 2025 · 1 revision

Multimodule with Maven 4

Terminology note: in this page, "module" means Java module, i.e. Java code having a module-info.java file. Maven 3 used the "module" word for something different, which is called "project" or "sub-project" in this page. Java modules are also known as the "Java Platform Module System" (JPMS). But this page does not use the latter terms because Java modules are not a system built on top of Java. Java modules are constituent of the Java language.

Compatibility note: the discussion in this page applies only when Module Source Hierarchy is used. Projects that do not use the Module Source Hierarchy continue to behave as in Maven 3. These projects are not impacted by the behavioral changes discussed below.

Introduction

This page describes how to organize a Maven project for taking advantage of the new multi-module support of Maven 4. In Maven, projects can be organized as a tree where each project contains an arbitrary number of sub-projects. In Maven 3, it was necessary to create a sub-project for each module. These sub-projects were typically declared as children of a parent project. In Maven 4, a project can contain a group of modules without sub-projects. Therefore, a multi-module project can be organized as a single parent project with no children.

This new organization does not change the execution flow of Maven plugins, but changes the execution flow of Java tools because more files are processed by each execution of a Maven plugin. In Maven 3, each sub-project is first compiled, then tested, and then packaged in a JAR file before to process the next sub-project. In Maven 4, all modules in a project or sub-project are compiled together, then tested together, then packaged in a series of JAR files. The compilation is generally faster since javac is launched only once (except for test classes and multi-releases). Tests can also be faster since there are more opportunities for JUnit parallel execution. Despite being tested together, each module continues to behave as if it has its own module-path, because only the dependencies declared in the requires statements of module-info.java are visible.

Multi-module and multi-project can be used together: a Maven project can have an arbitrary number of sub-projects that are themselves multi-module. It is up to the developer to choose what to organize as multi-modules and what to organize as multi-projects. As a rule of thumb, modules that are documented together with aggregated Javadoc are good candidates for regrouping in a single multi-module project. For simplicity, this page describes a single multi-module Maven project without sub-project. But most of this discussion is also applicable to sub-projects.

Project header

The following snippet gives a project skeleton. Dependencies, modules and plugin configurations will be added later in this page. This pom.xml is almost the same as Maven 3, except for the POM version number which is 4.1.0 instead of 4.0.0 and for the root = "true" attribute. Maven 4 uses that attribute for identifying the root of a tree of sub-projects. Even if this page does not use Maven multi-project feature, adding this attribute can help to avoid error messages.

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Maven project configuration file
  http://maven.apache.org/
-->
<project root = "true"
    xmlns              = "http://maven.apache.org/POM/4.1.0"
    xmlns:xsi          = "http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation = "http://maven.apache.org/POM/4.1.0
                          http://maven.apache.org/xsd/maven-4.1.0.xsd">

  <groupId>org.foo</groupId>
  <artifactId>my-project</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>My modular project</name>
</project>

If the project contains a <parent> declaration, often the parent fixes the compiler source and target versions to some predefined values. Those values need to be removed because they conflict with the new way to specify Java releases. Their removal can be done in our project by adding the following fragment:

  <properties>
    <maven.compiler.source/>    <!-- Remove the value inherited from parent POM. -->
    <maven.compiler.target/>
  </properties>

It is okay to keep <maven.compiler.release> and <maven.compiler.testRelease> properties, although they should be understood as a way to define default values only.

Dependencies declaration

For a project without sub-project, it is not necessary to put a <dependencyManagement> section. All dependencies can be declared in a single <dependencies> section, even if different modules use different subsets of the dependencies. Said otherwise, in a multi-module project, the <dependencies> section effectively behaves as dependency management.

Each dependency can be placed either on the class-path or on the module-path. This choice has deep consequences on reflection, on the way that services are found (using META-INF/services/ versus using module-info.class), on which classes will be visible to which module, on whether two modules can use the same package name, etc. By default, Maven makes an automatic choice based on heuristic rules. However, the result is sometime hard to predict and not always the best choice. For determinism and clarity, we recommend to always specify explicitly where to place each dependency with a <type> element having the classpath-jar or modular-jar value. Unless compatibility constraints require a progressive transition, a good strategy is to declare the modular-jar type for all dependencies, even the non-modular ones. In the latter case, an automatic name is inferred by Java based on the JAR filename, which requires to make sure that these files are not renamed. Example:

  <dependencies>
    <dependency>
      <groupId>org.foo</groupId>
      <artifactId>bar</artifactId>
      <version>1.2</version>
      <type>modular-jar</type>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.14.1</version>
      <type>modular-jar</type>
      <scope>test</scope>
    </dependency>
  </dependencies>

If two modules requires the same dependency with different scopes, use the strongest scope. For example, if a module uses a dependency with the compile scope and another module uses the same dependency with the test scope, then declare the compile (default) scope. The actual dependencies of each module will be the intersection of <dependencies> and module-info, which makes the scope less meaningful. In this context, a test scope (for example) is merely a way to avoid accidental use of a dependency in the main code of a module.

You will need the name of the Java module of each dependency in the next sections. If the JAR files were produced by the method described in this page, then the artifact name (ignoring version number) should match the Java module name. Otherwise, the module name can be obtained as below (it works also for automatic modules):

jar --describe-module --file <path to the JAR file>

Source directories of main code

For a multi-module project, declaring the modules is mandatory. There is no Maven convention that activate automatically the multi-module mode. Modules are declared in a <source> element which can be repeated as often as desired. The name declared in each <module> element must be the Java name of the module to compile. Example:

  <build>
    <sources>
      <source>
        <module>org.foo.module1</module>
      </source>
      <source>
        <module>org.foo.module2</module>
      </source>
    </sources>
  <build>

By convention, the main Java source files of each module are expected in the src/<module>/main/java directory. However, it is not necessary to organize a project with this directory layout. In particular, when migrating a Maven 3 multi-project to a Maven 4 multi-module project, it is okay to keep the Maven 3 directory structure unchanged and instead declare the directory of each module in a <directory> element. For example, if module1 and module2 were two Maven sub-projects, then a migration can be done as below:

  • Delete module1/pom.xml and module2/pom.xml.
  • Delete <subprojects> (formerly <modules>) in the root pom.xml.
  • Declare the sources as below:
  <build>
    <sources>
      <source>
        <module>org.foo.module1</module>
        <directory>module1/src/main/java</directory>
      </source>
      <source>
        <module>org.foo.module2</module>
        <directory>module2/src/main/java</directory>
      </source>
    </sources>
  <build>

Adding module-info

Source directories of test code

Patching module-info

Archive

The JAR plugin configuration should not contain an <addClasspath>true</addClasspath> declaration since the dependencies should not be placed on the class-path (they should be on the module-path).

If the plugin configuration contains a <mainClass> entry, the value should specify the module of the main class using the {@code "module/qualified.class.name"} syntax.

Clone this wiki locally