Introducing composable, module system native and agent friendly command line tools for modern Java development
By Danny Thomas, JVM Ecosystem Team
Recent work on the Java language to pave the on-ramp has made it easier than ever to start a Java program and evolve it using the full language and platform. At the end of that on-ramp lies Java’s mature build and dependency management ecosystem, capable of carrying software to enormous scale and complexity.
That ecosystem reached its maturity by developing strong models for projects, dependencies, and builds. When the Java Module System arrived, those models were already serving developers exceptionally well. The module descriptor consequently became just another description of the project to keep in agreement.
We’re excited to announce a preview of ja and its family of composable tools, that build on the capabilities of the Java Module System to provide a modern command line development experience for Java. We take the module descriptor and make it a complete description of a project, with dependency versions sitting naturally beside its requires
directives and module metadata provided through documentation tags:
/**
* @mainClass com.example.application.Main
*/
module com.example.application {
requires com.example.framework; // @1.2.3
}
Combined with command line ergonomics you’re used to in other languages, creating and consuming Java modules has never been easier.
Composable Tools
Java developers have long been exceptionally well served by graphical tools. An IDE formats source, navigates between declarations and usages, presents API documentation, and maintains a compiled view of the project. That experience has been so complete that Java has had less need to expose the same capabilities through small, composable command line tools. Those gaps become quickly apparent when coding agents work with the Java language, with agents frequently struggling to locate dependencies, documentation and sources.
ja
only provides command line ergonomics and tool orchestration, each feature is underpinned by a standalone tool. You don’t need to adopt ja
to get the benefit of these tools, you can compose them in any way you choose:
- jig performs module version resolution, compilation and assembly, outputting standard module system arguments for use with other tools. It is also the bridge to and from Maven repositories providing a standalone module proxy and publishing commands
- jfmt formats source using the Code Conventions for the Java Programming Language, adapted for the modern Java language. Avoids the very common whitespace, indentation, import ordering and qualified class references introduced in agent written code
- jist provides source aware symbol search, providing a
grep
style interface for understanding class files and their associated sources. Gives coding agents access to symbols and sources without indexing, LSPs or MCPs while interoperating with other build tools via an argument file contract - jdocserver serves locally browsable API documentation
These projects use the tool discovery and execution capabilities of the platform, and are intended to be installed in your JDK along with the standard tools. They all implement Tool or ToolProvider, allowing them to be run in process.
This is also the tool discovery and execution model for ja
. There we use OptionChecker and optional custom metadata to discover which module system options are supported so it can resolve the arguments on behalf of the tool. This provides a seamless transition from your source path modules to the standard JDK tooling such as jdeps
, jlink
and jshell
.
Maven as a foundation
In a recent survey of the 1,000 most popular artifacts on Maven Central, just 232 had explicit module definitions and another 248 declared automatic module names. The remaining 520 expressed no Java module name opinion. The module system also makes no distinction between namespace and module name, so module-first tooling requires a solution to module naming and location in existing repositories.
Get Netflix Technology Blog’s stories in your inbox
Join Medium for free to get updates from this writer.
Fortunately, Maven Central already gives published artifacts a verified namespace. Publishers prove control of reverse domain group IDs, reflecting Sonatype’s long standing case for namespaces in public repositories.
We use these conventions to establish a canonical Maven module coordinate, paring a verifiable DNS namespace with the complete module name, for example pkg:maven/com.netflix/com.netflix.tools.ja
. For existing modules, authors choose to publish a single Maven relocation pom
at the canonical coordinate, to allow for discovery of the original coordinate.
When neither are available, candidates are walked from the root of the namespace using common Maven artifact conventions inferring coordinates from module names. We also bundle a short list of aliases for the most popular modules that don’t use a reverse DNS module name, but we suggest authors should always namespace their modules. The module proxy in jig
presents resolved modules using the filename based conventions for module naming, making even automatic modules without stable names safe when used with these tools.
These conventions and location strategies allow the majority of existing artifacts to be discovered using only the module name and version.
Integrity by default
ALL-UNNAMED
has become unfortunately common in Java access options, because of the heavy use of the class path. It hides the source of the technical debt that applications are incurring by allowing such access and becomes increasingly consequential as Java moves toward Integrity by Default. For example, Preparing to Make Final Mean Final asks applications to explicitly authorize the modules allowed to mutate final fields.
We allow runtime access requirements to bedeclared as module metadata and carried with the module descriptor throughout the module’s lifecycle. For example a library may record the access it requires:
/**
* @enableFinalFieldMutation com.example.framework
*/
module com.example.framework {
}
However, the consuming application remains in control and must explicitly authorize the framework, for it to be available at runtime:
/**
* @mainClass com.example.application.Main
* @enableFinalFieldMutation com.example.framework
*/
module com.example.application {
requires com.example.framework; // @1.2.3
}
The command line interface for ja
allows the dependency and authorization to be added together:
ja require com.example.framework@1.2.3 \
--enable-final-field-mutation com.example.framework
Without that authorization, dependency resolution fails with an unsatisfied access requirement. Native access follows the same model through @enableNativeAccess
and qualified exports and opens are also supported.
Module integrity is ensured by persistent hashes of resolved binary dependencies in a module-info.hash
file, sequent resolution verifies those hashes and rejects an artifact that has changed.
We also take a step further than the recent improvements to annotation processor security by treating annotation processing as an explicit code generation step. The resulting sources are alongside regular module source, making them visible in code review and allowing a module to be assembled without executing generator code.
Make modules your default
We think every Java project should be modular, regardless of the build tool you’re using. If you’re a library author producing automatic modules, we’d encourage you to avoid split packages and produce explicit modules.
You can get started with our tools today with our installation guide.
Facts Only
* The Java Module System evolved existing models for projects, dependencies, and builds.
* `ja` and its family provide command line tools for modern Java development based on the Java Module System capabilities.
* The module descriptor is used to include project descriptions and dependency versions.
* Tools include `jig`, which handles module version resolution, compilation, assembly, and interaction with Maven repositories.
* `jfmt` formats source code using adapted Code Conventions for modern Java.
* `jist` provides source-aware symbol search functionality.
* `jdocserver` provides locally browsable API documentation.
* Tools implement the tool discovery and execution model via `OptionChecker` and custom metadata to resolve module system options.
* Module integrity is ensured by persistent hashes in a `module-info.hash` file to verify binary dependencies.
* Runtime access requirements can be declared as module metadata (e.g., `@enableFinalFieldMutation`).
* The command line interface allows combining dependency resolution and authorization, for example, using `ja require ... --enable-final-field-mutation`.
* Module integrity is verified by sequent resolution checking persistent hashes.
Executive Summary
The Java Module System provides a foundation for modern Java development, offering stronger models for projects, dependencies, and builds that align with the capabilities of the existing ecosystem. This evolution is introduced through command line tools like `ja` and its family, which leverage the module descriptor to create a complete project description including dependency information. These tools offer composable command line ergonomics that address gaps where graphical IDE tools are less effective, particularly for coding agents struggling with dependency resolution and source location.
The composable tools include specialized utilities such as `jig` for module version resolution and build orchestration, `jfmt` for formatting source code according to modern conventions, `jist` for symbol searching across sources, and `jdocserver` for API documentation. These tools discover and execute capabilities by implementing platform-defined models, allowing them to interact with standard JDK tooling like `jdeps`, `jlink`, and `jshell`.
The article also details strategies for maintaining module integrity through metadata, such as specifying runtime access requirements via annotations, which are enforced during dependency resolution. This includes using persistent hashes in `module-info.hash` files to verify binary dependencies and ensuring that annotation processing is treated as an explicit code generation step to improve visibility and review. The ultimate goal is to encourage a modular approach across all Java projects by making modules the default structure.
Full Take
The shift toward a module-first paradigm introduces tension between established repository practices and the need for verifiable, machine-readable metadata. The solution proposed attempts to reconcile this by layering verifiable coordination (using Maven coordinates and published artifact names) on top of module concepts, acknowledging that existing systems have favored namespace conventions over strict module naming in repositories.
The enforcement of integrity through runtime checks and hash verification establishes a novel mechanism for data provenance within the JVM ecosystem, moving beyond static analysis into runtime guarantees. This creates an expectation that developers must explicitly define security and access constraints at runtime, shifting responsibility from implicit path access to explicit authorization carried through the module lifecycle.
A significant implication is the re-framing of developer experience: command line tools are positioned as necessary complements to IDEs, suggesting that raw linguistic capability alone is insufficient for complex, scalable projects. The emphasis on composable tooling suggests a decentralized approach to development scaffolding, which contrasts with monolithic build systems. The challenge lies in ensuring that the mechanisms for establishing canonical coordinates and verifying module integrity do not introduce new points of failure or complexity that undermine the stated goal of making modularity the default. What assumptions are made about the ease with which developers will adopt this layered specification over deeply ingrained habits regarding classpath management?
