How to setup Maven for multi-component project

Maven is the most popular dependency management tool in the Java world with countless add-ons. Even if a project doesn’t use Maven for building, a lot of other build tools like Gradle, Bazel, Leiningen, or SBT use Maven repositories for keeping and obtaining libraries.

Suppose you have a project with the following structure:

pom.xml
src/
  main/
    java/
      org/
        example/
          core/
            Core.java
          util/
            FileUtil.java
          cli/
            Main.java
          web/
            Web.java

i.e. it’s some tool (e.g. static site generator named “generator”) with

  • web interface ;
  • command-line interface;
  • some core library (the main engine);
  • some filesystem utility library.

So the logical dependencies for this project look like the following:

The file utility library could be also used in other projects so it’s useful to keep it as a separate Maven artifact. Actually, the same is true for all other components. So let’s re-configure Maven to separate the components.

Step 1. Change the directories structure

Inside the main project let’s create a directory per component in the parent directory.

pom.xml
generator-web/
  pom.xml
  src/main/java/org/example/web/Web.java
generator-cli/
  pom.xml
  src/main/java/org/example/cli/Main.java
generator-core/
  pom.xml
  src/main/java/org/example/core/Core.java
generator-fileutil/
  pom.xml
  src/main/java/org/example/util/FileUtil.java

Each component’s pom.xml should be self-contained and should get dependencies via Maven. For example, generator-fileutil pom.xml could look like:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <groupId>org.example.generator</groupId>
    <artifactId>generator-fileutil</artifactId>
    <packaging>jar</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <organization>
        <name>Example Company</name>
        <url>http://example.org/</url>
    </organization>

    <name>File Utilities Library</name>
    <description>File Utilities Library</description>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.0.1</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-gpg-plugin</artifactId>
                <version>1.6</version>
                <executions>
                    <execution>
                        <id>sign-artifacts</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>sign</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
    </build>
    
    <distributionManagement>
        <repository>
            <id>target</id>
            <url>http://company.example.org/maven</url>
        </repository>
    </distributionManagement>
</project>

By “self-contained” I mean that one can go into the corresponding directory and build the package:
cd generator-fileutilmvn clean package

The <distributionManagement> tag specifies the URL of the company’s Maven repository to upload jar to. So the ideal organization looks like the following: whenever one pushes changes to a component, e.g. generator-fileutil

  • CI (continuous integration server) discovers the changes;
  • CI checks out the code and runs
    mvn clean package upload
  • This command uploads the new version of the JAR to the company’s Maven repository and since then it’s available for other components.
  • Other components (like generator-core ) have generator-fileutil as a dependency in their pom.xml files:
    <dependency> <groupId>org.example.generator</groupId> <artifactId>generator-fileutil</artifactId> <version>1.0.0-SNAPSHOT</version> <scope>compile</scope></dependency>

The maven-source-plugin allows to upload source JAR file into the company Maven repository as well, so the IDE will recognise and attach generator-fileutil sources when working with other components.

Now one can go inside any other component and build them with mvn clean package regardless of the fact whether generator-fileutil sources are located in a nearby directory or not. Thus we decouple dependency management and source management.

Step 2. The parent project pom.xml

There’s one problem remaining after Step 1. It’s difficult to change related components (e.g. generator-fileutil and generator-core ) at the same time. Indeed to change a signature of some utility method in generator-fileutil and its usages in generator-core one has to

  • Change and push generator-fileutil .
  • Wait until CI runs all the tests and uploads new version of generator-fileutil to the Maven.
  • Get the updated version of generator-fileutil from the Maven.

This is inconvenient, especially if you have to do that many times. So the encapsulating parent pom.xml is the rescue. Let’s look at our structure once again:

pom.xmlgenerator-web/generator-cli/generator-core/generator-fileutil/

This pom.xml allows to include the components as Maven modules so that the local sources of each component are used. Here’s how this pom.xml could look like:

<modelVersion>4.0.0</modelVersion>
    <groupId>org.example.generator</groupId>
    <artifactId>generator-project</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <organization>
        <name>Example company</name>
        <url>http://example.org/</url>
    </organization>

    <name>Generator App Project</name>
    <description>Generator App Aggregate Project</description>

    <modules>
        <module>generator-fileutil</module>
        <module>generator-core</module>
        <module>generator-cli</module>
        <module>generator-web</module>
    </modules>
</project>```

That’s all. Now if one runs

mvn clean package

for that parent directory, the local sources version of each component will be used. But the main advantage is that one can open the pom.xml as a project in the IDE and all the refactorings and compilation will run on the entire codebase.