Skip to content

Java Systems Engineering: Build Toolchains, JPMS Modules & Industrial Testing

Created: 2026-11-07 Last updated: 2026-11-07

Executive Summary: Enterprise Java applications are not compiled with ad-hoc compiler commands; they rely on industrial build automation systems, explicit modular encapsulation, and automated test harnesses. The Java Platform Module System (JPMS) enforces boundary security and reliable configuration across application layers, while build engines orchestrate multi-phase lifecycles from compilation to artifact verification. This guide forms Part 1.2 (Fundamentals) of our Java Systems Engineering curriculum: deconstructing Maven and Gradle build lifecycles, configuring module-info.java encapsulation barriers, engineering parameterized test suites with JUnit 5, and building a modular IPv4 CIDR subnet calculation engine.

  • Maven & Gradle Lifecycles


    Deconstruct standard directory layouts, deterministic lifecycle phases, transitive dependency DAGs, and build cache mechanics.

  • JPMS (Project Jigsaw)


    Eliminate classpath hell through strong encapsulation, exports, requires transitive, and reflective runtime opens directives.

  • Industrial Testing (JUnit 5 & Mockito)


    Write high-coverage test suites utilizing @ParameterizedTest, @CsvSource, lifecycle hooks, and Mockito 5 test doubles.

  • IPv4 Subnet Calculator Lab


    Construct a production-ready modular Maven project implementing bitwise IP mathematics and automated test verification.


1. Build Toolchains Under the Hood: Maven vs. Gradle

Enterprise Java ecosystems rely primarily on two build automation engines: Apache Maven and Gradle.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph MavenEngine ["Apache Maven (Standard Lifecycle)"]
        direction TB
        M_VAL["1. validate<br/>Verify project correctness"]
        M_COM["2. compile<br/>Compile src/main into target/classes"]
        M_TST["3. test<br/>Run unit tests with Surefire"]
        M_PKG["4. package<br/>Bundle classes into JAR / WAR"]
        M_VRF["5. verify<br/>Run integration checks (Failsafe)"]
        M_INS["6. install<br/>Copy artifact to ~/.m2 repository"]
        M_VAL --> M_COM --> M_TST --> M_PKG --> M_VRF --> M_INS
    end

    subgraph GradleEngine ["Gradle (DAG Task Graph)"]
        direction TB
        G_CFG["Configuration Phase<br/>Evaluate build.gradle.kts"]
        G_DAG["Build Directed Acyclic Graph<br/>Task dependency resolution"]
        G_EXE["Execution Phase<br/>Execute only dirty tasks (Cache)"]
        G_CFG --> G_DAG --> G_EXE
    end

Standard Maven Directory Layout

Maven standardizes project layout across the global Java industry. Consistency eliminates configuration overhead:

my-app/
├── pom.xml                      # Project Object Model descriptor
└── src/
    ├── main/
    │   ├── java/                # Production Java source code
    │   └── resources/           # Configuration files, properties, SQL
    └── test/
        ├── java/                # Test source code (JUnit / Mockito)
        └── resources/           # Test fixtures and mock data

Dependency Coordinates & Conflict Resolution

Maven identifies dependencies via coordinates: GroupId:ArtifactId:Version (GAV).

When conflicting versions of the same library appear in a transitive dependency tree, Maven uses a deterministic rule: Nearest-Definition-Wins (the dependency closest to the root in the tree is chosen). Gradle, by contrast, resolves conflicts using Highest-Version-Wins by default.


2. The Java Platform Module System (JPMS / Project Jigsaw)

Introduced in Java 9, the Java Platform Module System (JPMS) transitioned Java from a flat, unvalidated classpath to an explicitly defined module graph with strong encapsulation.

Classpath Hell vs. JPMS Modular Boundaries

  • Flat Classpath (-cp): All JARs are thrown into a single bucket. Missing classes trigger runtime ClassNotFoundException. Duplicate classes cause non-deterministic behavior depending on JAR load order. Private internal classes can be accessed arbitrarily via reflection.
  • Module Path (--module-path): The JVM verifies that all required modules are present before application boot. Classes are strictly invisible to other modules unless explicitly exported in module-info.java.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph ModuleA ["Module: com.network.core"]
        EXP["Exported Package:<br/>com.network.core.api"]
        INT["Encapsulated Internal Package:<br/>com.network.core.internal"]
    end

    subgraph ModuleB ["Module: com.network.cli"]
        CLI["CLI Consumer Service"]
    end

    CLI ==>|"Permitted (Compile & Runtime)"| EXP
    CLI -.->|"BLOCKED by JVM Encapsulation"| INT

Directives in module-info.java

module com.network.diagnostics {
    // 1. Module dependencies (readability graph)
    requires java.base;       // Implicitly included in all modules
    requires java.net.http;   // External standard JDK module

    // 2. Transitive readability: any module that requires this module 
    // automatically gets readability access to org.slf4j
    requires transitive org.slf4j;

    // 3. Strong Encapsulation: Only public types in this package are exposed
    exports de.nejathakan.netdiag.api;

    // 4. Qualified Export: Only accessible to the monitoring agent module
    exports de.nejathakan.netdiag.internal to com.infrastructure.monitor;

    // 5. Open for Reflection: Permitted for deep reflection (required by JUnit & Jackson)
    opens de.nejathakan.netdiag to org.junit.platform.commons;
}

3. Industrial Testing Architecture: JUnit 5 & Mockito 5

In professional software development, unverified code is non-deployable code. Java relies on JUnit 5 (Jupiter) and Mockito 5.

JUnit 5 Architecture

JUnit 5 decouples the test platform from the programming model: 1. JUnit Platform: Foundation for launching testing frameworks on the JVM. 2. JUnit Jupiter: The modern API and extension model for writing tests in Java 8+. 3. JUnit Vintage: Backwards-compatibility engine to execute legacy JUnit 3 and 4 tests.

Parameterized Testing (@ParameterizedTest)

Parameterized tests execute the same verification logic across diverse datasets without code duplication:

Parameterized Test with CSV Data Sources
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MathTest {

    @ParameterizedTest(name = "Prefix /{0} should equal netmask {1}")
    @CsvSource({
        "24, 255.255.255.0",
        "16, 255.255.0.0",
        "8,  255.0.0.0",
        "30, 255.255.255.252",
        "32, 255.255.255.255"
    })
    void testPrefixToNetmaskConversion(int prefix, String expectedMask) {
        assertEquals(expectedMask, calculateNetmask(prefix));
    }

    private String calculateNetmask(int prefix) {
        int mask = prefix == 0 ? 0 : 0xFFFFFFFF << (32 - prefix);
        return String.format("%d.%d.%d.%d",
            (mask >>> 24) & 0xFF,
            (mask >>> 16) & 0xFF,
            (mask >>> 8) & 0xFF,
            mask & 0xFF
        );
    }
}

4. Hands-On Systems Lab: network-diagnostics IPv4 Subnet Calculator

We will now build a fully reproducible, standalone Maven project with JPMS modularization and comprehensive JUnit 5 parameterized test coverage: - Performs bitwise unsigned 32-bit arithmetic for IPv4 addresses. - Calculates network address, broadcast address, netmask, and usable host count. - Complies with RFC 3021 (point-to-point /31 subnets) and single host /32 configurations. - Demonstrates complete pom.xml, module-info.java, production classes, and automated test suites.

Project Layout on Disk

network-diagnostics/
├── pom.xml
└── src/
    ├── main/
    │   └── java/
    │       ├── module-info.java
    │       └── de/nejathakan/netdiag/
    │           ├── SubnetDetails.java
    │           └── SubnetCalculator.java
    └── test/
        └── java/
            └── de/nejathakan/netdiag/
                └── SubnetCalculatorTest.java

Step 1: Project Descriptor (pom.xml)

network-diagnostics/pom.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 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>de.nejathakan</groupId>
    <artifactId>network-diagnostics</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>21</maven.compiler.release>
        <junit.jupiter.version>5.10.2</junit.jupiter.version>
    </properties>

    <dependencies>
        <!-- JUnit 5 Jupiter Aggregator -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin configured for Java 21 LTS -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>21</release>
                </configuration>
            </plugin>

            <!-- Maven Surefire Plugin for executing JUnit 5 tests on JPMS -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: The Module Descriptor (module-info.java)

network-diagnostics/src/main/java/module-info.java
module de.nejathakan.netdiag {
    // Export public API package to consumers
    exports de.nejathakan.netdiag;

    // Open package to JUnit 5 reflection engine during test execution
    opens de.nejathakan.netdiag to org.junit.platform.commons;
}

Step 3: Immutable Data Model (SubnetDetails.java)

network-diagnostics/src/main/java/de/nejathakan/netdiag/SubnetDetails.java
package de.nejathakan.netdiag;

/**
 * Immutable carrier record encapsulating computed IPv4 subnet telemetry.
 */
public record SubnetDetails(
    String cidrInput,
    String ipAddress,
    int prefixLength,
    String netmask,
    String networkAddress,
    String broadcastAddress,
    String firstUsableIp,
    String lastUsableIp,
    long totalHosts,
    long usableHosts
) {}

Step 4: Bitwise Subnet Math Engine (SubnetCalculator.java)

network-diagnostics/src/main/java/de/nejathakan/netdiag/SubnetCalculator.java
package de.nejathakan.netdiag;

import java.util.regex.Pattern;

public class SubnetCalculator {

    private static final Pattern CIDR_PATTERN = Pattern.compile(
        "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
    );

    /**
     * Parses an IPv4 CIDR string and returns calculated subnet metadata.
     *
     * @param cidr CIDR string (e.g. "192.168.1.50/24")
     * @return SubnetDetails with exact network addresses and host boundaries
     */
    public SubnetDetails calculate(String cidr) {
        if (cidr == null || !CIDR_PATTERN.matcher(cidr.trim()).matches()) {
            throw new IllegalArgumentException("Invalid CIDR format. Expected IP/Prefix (e.g. 192.168.1.0/24)");
        }

        String[] parts = cidr.trim().split("/");
        String ipStr = parts[0];
        int prefix = Integer.parseInt(parts[1]);

        int ipInt = parseIpv4StringToInt(ipStr);
        int netmaskInt = prefix == 0 ? 0 : 0xFFFFFFFF << (32 - prefix);
        int networkInt = ipInt & netmaskInt;
        int broadcastInt = networkInt | ~netmaskInt;

        long totalHosts = 1L << (32 - prefix);
        long usableHosts;
        String firstUsable;
        String lastUsable;

        if (prefix == 32) {
            usableHosts = 1;
            firstUsable = formatIntToIpv4(networkInt);
            lastUsable = formatIntToIpv4(networkInt);
        } else if (prefix == 31) {
            // RFC 3021 Point-to-Point Links
            usableHosts = 2;
            firstUsable = formatIntToIpv4(networkInt);
            lastUsable = formatIntToIpv4(broadcastInt);
        } else {
            usableHosts = totalHosts - 2;
            firstUsable = formatIntToIpv4(networkInt + 1);
            lastUsable = formatIntToIpv4(broadcastInt - 1);
        }

        return new SubnetDetails(
            cidr.trim(),
            ipStr,
            prefix,
            formatIntToIpv4(netmaskInt),
            formatIntToIpv4(networkInt),
            formatIntToIpv4(broadcastInt),
            firstUsable,
            lastUsable,
            totalHosts,
            usableHosts
        );
    }

    // Converts IPv4 "X.X.X.X" string to a 32-bit signed integer
    public static int parseIpv4StringToInt(String ip) {
        String[] octets = ip.split("\\.");
        int result = 0;
        for (int i = 0; i < 4; i++) {
            int octet = Integer.parseInt(octets[i]);
            if (octet < 0 || octet > 255) {
                throw new IllegalArgumentException("Octet out of range [0-255]: " + octet);
            }
            result = (result << 8) | octet;
        }
        return result;
    }

    // Converts a 32-bit integer back to dotted-decimal notation
    public static String formatIntToIpv4(int ip) {
        return String.format("%d.%d.%d.%d",
            (ip >>> 24) & 0xFF,
            (ip >>> 16) & 0xFF,
            (ip >>> 8) & 0xFF,
            ip & 0xFF
        );
    }
}

Step 5: Industrial JUnit 5 Test Suite (SubnetCalculatorTest.java)

network-diagnostics/src/test/java/de/nejathakan/netdiag/SubnetCalculatorTest.java
package de.nejathakan.netdiag;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("IPv4 Subnet Calculator Test Suite")
class SubnetCalculatorTest {

    private SubnetCalculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new SubnetCalculator();
    }

    @Nested
    @DisplayName("Standard Subnet Calculations")
    class StandardSubnetTests {

        @ParameterizedTest(name = "{0} -> Net: {1}, Mask: {2}, Hosts: {3}")
        @CsvSource({
            "192.168.1.100/24, 192.168.1.0, 255.255.255.0, 192.168.1.255, 254",
            "10.0.50.1/16,     10.0.0.0,     255.255.0.0,   10.0.255.255,   65534",
            "172.16.8.99/20,   172.16.0.0,   255.255.240.0, 172.16.15.255,  4094",
            "192.168.0.1/30,   192.168.0.0, 255.255.255.252, 192.168.0.3,   2"
        })
        void testValidSubnetCalculations(
            String cidr,
            String expectedNet,
            String expectedMask,
            String expectedBcast,
            long expectedUsable
        ) {
            SubnetDetails details = calculator.calculate(cidr);

            assertAll("Subnet Attributes",
                () -> assertEquals(expectedNet, details.networkAddress()),
                () -> assertEquals(expectedMask, details.netmask()),
                () -> assertEquals(expectedBcast, details.broadcastAddress()),
                () -> assertEquals(expectedUsable, details.usableHosts())
            );
        }
    }

    @Nested
    @DisplayName("RFC 3021 and Edge Case Subnets")
    class EdgeCaseSubnetTests {

        @Test
        @DisplayName("RFC 3021 /31 Subnet has 2 usable hosts and no broadcast")
        void testRfc3021Subnet() {
            SubnetDetails details = calculator.calculate("10.0.0.0/31");
            assertEquals("10.0.0.0", details.firstUsableIp());
            assertEquals("10.0.0.1", details.lastUsableIp());
            assertEquals(2, details.usableHosts());
        }

        @Test
        @DisplayName("/32 Host Route has 1 host and matches input IP")
        void testSingleHostRoute() {
            SubnetDetails details = calculator.calculate("192.168.1.1/32");
            assertEquals("192.168.1.1", details.networkAddress());
            assertEquals("255.255.255.255", details.netmask());
            assertEquals(1, details.usableHosts());
        }
    }

    @Nested
    @DisplayName("Validation and Exception Handling")
    class ExceptionHandlingTests {

        @ParameterizedTest(name = "Invalid CIDR: {0}")
        @ValueSource(strings = {
            "invalid-ip",
            "256.0.0.1/24",
            "192.168.1.1/33",
            "10.0.0.1/-1",
            "",
            "192.168.1.1"
        })
        void testInvalidCidrInputs(String invalidInput) {
            assertThrows(IllegalArgumentException.class, () -> calculator.calculate(invalidInput));
        }
    }
}

Executing and Verifying the Project on Linux

Open a terminal in the network-diagnostics directory and execute:

Compiling and Running Automated Tests
mvn clean test

Expected Output:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running de.nejathakan.netdiag.SubnetCalculatorTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.128 s -- in de.nejathakan.netdiag.SubnetCalculatorTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------


Where to Go Next

With build automation, module encapsulation, and rigorous testing mastered, proceed to advanced runtime structures and type mechanics:

  • Part 2.1: Collections Framework, Generics & Memory Model


    Explore HashMap bucket treeification, Generics covariance and contravariance (PECS), and engineer an LRU Cache.

    Advance to Part 2.1