Best Practices for Modular Development in the Context of Maven Core Concepts and Build Process

The modular system introduced in Java 9 (Project Jigsaw) has fundamentally transformed the way Java applications are built, aiming to address dependency chaos, version conflicts, and encapsulation issues. While Maven's plugin ecosystem remains powerful, a deep understanding of the core concepts of the module system is essential for modern development within the Maven environment.

A Java module consists of two parts: the declaration part and the implementation part. The declaration part defines the module's metadata, including the module name, provider, and required interfaces (requires); the implementation part contains the actual source code files. Through the module-info.java file, developers can precisely control which classes are accessible by other modules and which interfaces must be exported. This mechanism ensures that dependency errors are caught at compile time, avoiding runtime crashes caused by missing dependencies or version mismatches.
Introducing modular development into Maven projects hinges on correctly configuring the pom.xml file to support multi-module structures and explicitly defining the dependency relationships between each sub-module. Maven natively supports multi-module projects; one simply needs to declare the coordinates of the sub-modules in the parent POM. For a single Java 9+ module project, the <modules> tag must still be specified in the POM, but the core difference lies in the structure of the source code directories and the compilation configuration.

Here is a standard configuration example for a Maven multi-module project:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>parent-project</artifactId>
    <version>1.0.0</version>
    
    <packaging>pom</packaging>

    <modules>
        <module>core-module</module>
        <module>web-module</module>
    </modules>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
</project>

In the POM of the sub-module core-module, it should define the interfaces it provides for use by other modules:

<groupId>com.example</groupId>
<artifactId>core-module</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>17</source>
                <target>17</target>
            </configuration>
        </plugin>
    </plugins>
</build>

Dependency Management and Version Control Between Modules

The core advantage of modular development lies in strict dependency management. Through requires and exports directives, developers can restrict visibility between modules. For instance, if Module A depends on Module B, it must declare module B requires module A;; while Module B needs to expose interfaces externally, it should use exports com.example.api;.

During the Maven build process, the dependency transitivity mechanism remains valid, but two specific points require special attention:

  1. Explicit Dependency Declaration: Each module should explicitly list its direct dependencies to avoid the risks of invisibility brought by implicit dependencies.
  2. Version Locking: Since the module system requires precise version matching, it is recommended to use <dependencyManagement> in pom.xml to uniformly standardize dependency versions for all sub-modules, preventing inconsistent third-party libraries from being introduced by different modules.

Example: Managing core library versions centrally in the parent POM

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.9</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Build Process Optimization and Best Practices

After completing the module division, the build process must be optimized to ensure development efficiency. First, enabling Maven's parallel compilation and packaging features can significantly improve the build speed of large projects. Second, configuring maven-jar-plugin with the correct output directory structure ensures that each module's JAR package contains complete module information internally. Finally, integrating code scanning tools (such as SpotBugs or PMD) to check for potential module access violations during the build phase.

Additionally, it is recommended to write independent unit tests for each module and execute the mvn clean install command in the CI/CD pipeline to verify the integrity of the entire project's modules. For cross-platform deployment, attention must also be paid to compatibility configuration for JEP 351 (Java Module System) to ensure that generated JAR packages run correctly on different JDK versions.

By adhering to the above best practices for modular development, teams can not only improve code maintainability and security but also effectively reduce the rate of online failures caused by dependency conflicts, driving Java applications toward more modern architectures.