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.javaencapsulation 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 runtimeopensdirectives. -
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 runtimeClassNotFoundException. 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 inmodule-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:
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)
<?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)
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)
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)
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)
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:
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
HashMapbucket treeification, Generics covariance and contravariance (PECS), and engineer an LRU Cache.