Skip to content

Module Layout

Danny Thomas edited this page Sep 17, 2026 · 1 revision

Module layout

A source module is a directory containing module-info.java, with Java source and resources arranged beneath it by package:

 -- module-info.java
`-- com/example/application/
    |-- Application.java
    `-- messages.properties

There is no required separation between production and test sources and resources. Colocating tests and benchmarks with the implementation is recommended when they need access to package-private code. Tools define class and package name suffix conventions that identify content to filter out when packaging a module.

Dedicated test and benchmark modules with ordinary requirements are a better fit when they use only public APIs. A multi-release layout is also supported for release-specific source and resources.

Module source paths

For a layout more suitable for source control or to support multiple modules, place each module using the module source path convention in src:

src/
|-- com.example.api/
|   `-- module-info.java
|-- com.example.application/
|   `-- module-info.java
`-- com.example.application.test/
    `-- module-info.java

From the repository root, from src itself, or from anywhere inside an existing module source path, ja init creates the new module beneath src:

ja init com.example.application

A directory named src is treated as a module source path when it is empty or contains the standard <module>/module-info.java structure. An unrelated layout such as src/main/java is left alone. The directory structure and module declarations are enough to discover the modules; there is no additional build file to maintain.

The working directory continues to select scope: run from the repository root or src to include every module, and from a module directory or one of its descendants to select that module. Use -C to make the same choice without first changing directories:

ja compile
ja -C src/com.example.application compile

Tests and benchmarks

For white-box tests, you can keep tests and their resources alongside the implementation:

com.example.application/
|-- module-info.java
`-- com/example/application/
    |-- Application.java
    |-- ApplicationTest.java
    `-- test/
        |-- Fixture.java
        `-- fixture.properties

Add JUnit as a static requirement so it is available when compiling and testing, but is not required when the application runs:

ja require --static org.junit.jupiter

When the module is packaged, the class and package suffixes declared by the tool determine which supporting content is left out.

For black-box tests against exported APIs, use a dedicated module with ordinary requirements. Making JUnit a regular requirement ensures that tests and resources remain in the packaged module:

module com.example.application.test {
    requires com.example.application;
    requires org.junit.jupiter; // @6.1.3
}

Benchmarks follow the same pattern. Add JMH as a static requirement when benchmarks live with the implementation:

ja require --static org.openjdk.jmh.core

The requirement activates the benchmark runner. Use an ordinary requirement on org.openjdk.jmh.core in a dedicated benchmark module.

JMOD layout

Use the standard JMOD sections for modules that contain more than classes and resources:

com.example.tool/
|-- bin/
|-- classes/
|   |-- module-info.java
|   `-- com/example/tool/Tool.java
|-- conf/
|-- include/
|-- legal/
|-- lib/
`-- man/

Java source and resources go beneath classes. Only the sections used by the module need to be present. Exporting a module with this layout produces a jmod in addition to its jar.

Resources and multi-release modules

Keep resources alongside the Java source that uses them. Their module-relative paths are preserved when the module is packaged.

Use META-INF/versions/<release> for release-specific source and resources:

com.example.application/
|-- META-INF/MANIFEST.MF
|-- META-INF/versions/21/com/example/application/
|   |-- Application.java
|   `-- messages.properties
|-- module-info.java
`-- com/example/application/
    |-- Application.java
    `-- messages.properties

Set Multi-Release: true in the manifest. For the selected release, files beneath META-INF/versions/<release> override matching files in the base module.

Clone this wiki locally