Modules and the build
Java code is organised at four levels: classes, which group fields and methods; packages, which group classes; modules (since Java 9), which aggregate packages with explicit dependencies and exports; and artifacts (typically jars, the compiled output), which are the units of deployment. The build is conventionally managed by Maven or Gradle, both of which manage dependencies declared against repositories such as Maven Central. The mainstream JVM is OpenJDK; commercial distributions include Oracle JDK, Amazon Corretto, Azul Zulu, and Eclipse Temurin. This page covers the compilation pipeline, the classpath and module-path, modules and the JPMS, jar files, the build systems, and the conventions of Java project organisation.
The compilation pipeline
A Java program is built in three principal stages:
- Compilation — the
javaccompiler parses the source files, type-checks, and emits class files (one per class) containing JVM bytecode. - Packaging — the class files are packaged into one or more jar files (Java archives), each a zip-format archive plus a manifest.
- Execution — the JVM loads classes on demand from the jar files, just-in-time-compiles bytecode to native code, and executes.
The conventional command-line tools:
javac MyClass.java # compile to MyClass.class
java MyClass # run
jar cf myapp.jar MyClass.class # package into a jar
java -jar myapp.jar # run from a jar
javadoc -d docs MyClass.java # generate API documentation
Each stage is implemented by a separate tool; modern build systems (Maven, Gradle) orchestrate them.
Class files and bytecode
A class file is the binary form of a single Java type, conforming to the JVM specification. The format includes:
- A magic number (
0xCAFEBABE) identifying the file as a class file. - The major and minor version (e.g., 65.0 for Java 21).
- The constant pool — a table of constants (class names, method signatures, string literals, numeric constants) referenced by the bytecode.
- The access flags (public, final, abstract, etc.).
- The class hierarchy (this class, superclass, interfaces).
- The fields with their types and modifiers.
- The methods with their signatures, modifiers, and bytecode.
- Attributes — additional metadata (annotations, generic signatures, line-number information).
The bytecode is a stack-based instruction set; the JVM executes it through interpretation (initially) and just-in-time compilation (for hot paths).
The classpath
The classpath is the JVM’s mechanism for finding classes. It is a list of locations — directories (containing .class files in package subdirectories) and jar files — that the class loader searches:
java -cp myapp.jar:lib/*:classes/ MyClass
The classpath separator is : on Unix-like systems and ; on Windows. Wildcards (lib/*) admit including all jars in a directory.
The principal limitations of the classpath model:
- No explicit dependency declaration — a class may use any class on the classpath; there is no compile-time check that the dependency is present.
- No version conflict resolution — if two jars on the classpath provide different versions of the same class, the class loader uses the first one found, often unpredictably.
- No encapsulation — a class is either visible to everyone on the classpath or not at all (subject to package-private restrictions).
The module system, introduced in Java 9, addresses these limitations.
The module system (Java 9, JPMS)
The Java Platform Module System (JPMS) introduces modules as units of strong encapsulation and explicit dependency. A module is declared by a module-info.java file at the module root:
module com.example.app {
requires java.base; // implicit; always present
requires java.sql;
requires com.example.utils;
exports com.example.app.api;
// packages not in 'exports' are invisible outside the module
}
The principal directives:
requires <module>— declares a dependency on another module. The module’s exported packages become available.requires transitive <module>— the dependency is re-exported; modules requiring this module also see the transitive module.exports <package>— makes the package visible to importing modules.exports <package> to <module1>, <module2>— qualified export; visible only to the listed modules.opens <package>— admits reflection access to the package (for frameworks such as Spring that use reflection).opens <package> to <module>— qualified open.uses <service-class>andprovides <service> with <impl>— service-provider declarations.
A module’s exported packages are visible to importing modules; a module’s non-exported packages are invisible regardless of whether their types are public. The mechanism gives Java a strong-encapsulation story comparable to namespaces in C# or pub in Rust.
The unnamed module and automatic modules
For backward compatibility:
- The unnamed module — code on the classpath (no
module-info.java) belongs to the unnamed module. It can read every other module but is not visible to named modules unless theyrequires java.base(which they always do). - Automatic modules — a jar file on the module path that has no
module-info.javabecomes an automatic module. Its name is derived from the jar’s filename or itsAutomatic-Module-Namemanifest attribute. It exports all its packages and reads every other module.
The mechanism admits gradual migration: existing jars work as automatic modules until they are explicitly modularised.
Service-provider mechanism
Modules declare services — interfaces with multiple implementations — and providers — modules that supply implementations:
// In the API module:
module com.example.api {
exports com.example.api.spi;
uses com.example.api.spi.Logger;
}
// In a provider module:
module com.example.console-logger {
requires com.example.api;
provides com.example.api.spi.Logger
with com.example.console.ConsoleLogger;
}
// In the consumer:
ServiceLoader<Logger> loaders = ServiceLoader.load(Logger.class);
for (Logger logger : loaders) {
logger.log("hello");
}
The service-loader mechanism is the conventional Java idiom for plugin architectures.
Adoption status
The module system shipped in Java 9 (2017). Adoption has been slow:
- The Java standard library itself is fully modularised;
java.base,java.sql,java.logging, etc., are modules. - Modern Java applications may or may not use modules; many ship as a single jar or a fat jar without
module-info.java. - Third-party libraries are inconsistent: some are modularised, others rely on automatic modules.
The conventional contemporary advice:
- Small applications and libraries: skip the module system; rely on the classpath.
- Large applications: adopt modules to gain compile-time dependency checking and runtime encapsulation.
- Libraries: provide a
module-info.javato admit modularised consumption; do not require modules for use.
Jar files
A jar is a zip-format archive containing class files plus metadata. The structure:
myapp.jar:
├── META-INF/
│ ├── MANIFEST.MF # metadata (main class, classpath, etc.)
│ └── services/ # service-provider configuration
├── com/
│ └── example/
│ ├── App.class
│ └── Helper.class
└── application.properties
The MANIFEST.MF carries:
Main-Class— the entry point forjava -jar.Class-Path— additional classpath for the jar’s runtime.Automatic-Module-Name— the module name when the jar is loaded as an automatic module.- Custom attributes for build tooling.
The jar command-line tool packages and inspects jars:
jar cf myapp.jar -C classes . # create
jar cfe myapp.jar com.example.Main -C classes . # create with main-class
jar tf myapp.jar # list contents
jar xf myapp.jar # extract
For executable jars, the conventional pattern is:
java -jar myapp.jar
The JVM reads the manifest’s Main-Class, loads the class, and invokes its main(String[]) method.
Multi-release jars
A multi-release jar (Java 9+) admits version-specific class files:
myapp.jar:
├── META-INF/
│ └── versions/
│ ├── 9/com/example/Helper.class # used by JVM 9+
│ └── 17/com/example/Helper.class # used by JVM 17+
├── com/example/Helper.class # used by JVM 8 and below
└── ...
The JVM picks the version-specific class file matching its own version (or falls back). The mechanism admits libraries that take advantage of newer JVM features while remaining compatible with older runtimes.
Maven
Maven is one of the dominant Java build systems. It uses a declarative XML configuration file (pom.xml) and a convention-over-configuration project layout:
myproject/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/ # production source
│ │ └── resources/ # production resources
│ └── test/
│ ├── java/ # test source
│ └── resources/ # test resources
└── target/ # build output
A minimal pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
The principal Maven commands:
mvn compile # compile main sources
mvn test # compile and run tests
mvn package # build the jar
mvn install # install the jar in the local Maven cache
mvn clean # remove build output
Maven downloads dependencies from repositories — typically Maven Central, with optional private and corporate mirrors. The local cache is ~/.m2/repository.
Gradle
Gradle is the other dominant Java build system. It uses a Groovy or Kotlin DSL for configuration, which is more flexible than Maven’s XML:
plugins {
`java`
`application`
}
group = "com.example"
version = "1.0.0"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
application {
mainClass.set("com.example.Main")
}
The principal Gradle commands:
./gradlew compileJava
./gradlew test
./gradlew build
./gradlew run
./gradlew clean
The ./gradlew wrapper script (the Gradle Wrapper) downloads the appropriate Gradle version and runs it; the convention admits per-project Gradle versions without requiring a system-wide install.
Project structure conventions
The Maven-style layout (src/main/java, src/test/java) is the de facto standard for both Maven and Gradle projects. A typical multi-module project:
myproject/
├── pom.xml (or settings.gradle) # parent / multi-module
├── api/
│ ├── pom.xml
│ └── src/main/java/...
├── impl/
│ ├── pom.xml
│ └── src/main/java/...
└── tests/
└── pom.xml
Each subdirectory is a sub-project with its own configuration, dependencies, and build output.
jar, jmod, jlink
Three distinct artifact types:
- jar — the conventional artifact for libraries and applications.
- jmod (Java 9+) — a module-specific archive containing native libraries and config files in addition to class files. Used for the JDK itself; rarely produced by user code.
- jlink (Java 9+) — a tool that produces a custom runtime image containing only the modules an application uses. The image is a stripped-down JVM packaged with the application.
jlink --module-path mods:$JAVA_HOME/jmods \
--add-modules com.example.app \
--output runtime/
# Run:
runtime/bin/java -m com.example.app/com.example.app.Main
The mechanism produces small, self-contained deployments — useful for containerised applications where the full JDK is overhead.
A note on the broader ecosystem
The Java ecosystem extends far beyond the core build tooling:
- Spring Framework / Spring Boot — the dominant web/application framework.
- Hibernate / JPA — ORM.
- Jackson, Gson — JSON serialisation.
- JUnit, TestNG — testing.
- SLF4J, Logback, Log4j2 — logging.
- Apache Commons, Guava — utility libraries.
- Reactor, RxJava — reactive programming.
- Micronaut, Quarkus — alternative web frameworks emphasising fast startup and AOT compilation.
- GraalVM — alternative JVM with native-image AOT compilation.
The conventional advice for new projects: start with dotnet new-style scaffolds (Spring Initializr, Gradle init, Maven archetypes); add dependencies as needed; rely on the standard library and Maven Central / Gradle’s repositories for the broader ecosystem.