Automatic Modules: Bridging Legacy and Modular Java
When Java 9 introduced the Java Platform Module System (JPMS) in 2017, it created an immediate problem. Millions of existing JAR files had no module descriptors. Organizations couldn’t simply abandon their dependencies overnight, yet they wanted to embrace the new module system’s benefits. The solution was automatic modules, a transitional mechanism that lets non-modular JARs behave like modules during migration.
Understanding automatic modules is crucial for anyone working with modern Java. They’re not perfect, and they come with limitations that can surprise developers. But for incremental migration from legacy codebases to fully modular applications, they provide the only practical path forward.
The Module System’s Compatibility Challenge
The module system fundamentally changed how Java manages dependencies and encapsulation. Modules explicitly declare what they require and what they export, creating strong boundaries between components. This reliability comes at a cost: modules can only depend on other named modules, not on arbitrary code from the classpath.
This creates tension with the existing ecosystem. Organizations using Spring, Hibernate, older JDBC drivers, or thousands of other libraries face a dilemma. These frameworks weren’t written with modules in mind, yet applications need them to function. Waiting for every library maintainer to modularize their code before starting migration would take years, if it happened at all.
Automatic modules address this by allowing non-modular JARs to participate in the module system without requiring any changes to the library itself. Place a regular JAR on the module path instead of the classpath, and it automatically becomes an automatic module with generated metadata.
How Automatic Modules Work
An automatic module is simply a plain JAR file without a module descriptor that’s placed on the module path. Since it lacks explicit metadata, the module system makes assumptions about what the module needs and provides. These assumptions prioritize compatibility over correctness.
Every package in the automatic module gets exported, whether intended for public use or not. This breaks encapsulation but ensures that code dependent on those packages continues working. The module’s dependencies become implicit: it reads all other modules on the module path plus everything on the classpath. This allows automatic modules to bridge between the modular world and legacy classpath, something regular modules cannot do.
The tricky part is naming. The module system determines an automatic module’s name through a two-step process. First, it checks the JAR’s manifest file for an “Automatic-Module-Name” entry. If present, that value becomes the module name. Otherwise, the name derives from the JAR filename through a transformation process that removes version numbers, converts non-alphanumeric characters to dots, and eliminates duplicate or trailing dots.
A JAR named “commons-lang3-3.12.0.jar” becomes the module “commons.lang3” through filename derivation. This works but creates instability. If the filename changes between versions, the module name changes too, breaking code that depends on it.
The Automatic-Module-Name Manifest Entry
Library maintainers can stabilize their module names by adding an entry to their JAR’s MANIFEST.MF file. This single line establishes a permanent module name without requiring full modularization. The module name should follow reverse DNS notation, using the longest shared prefix of all packages in the module, such as “com.acme.mylibrary” for a library whose packages begin with that prefix.
Adding this entry is straightforward with Maven:
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.example.library</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
Or with Gradle:
jar {
manifest {
attributes 'Automatic-Module-Name': 'com.example.library'
}
}
This is the minimum step library maintainers should take to support the module system. It requires no recompilation, doesn’t force upgrading to Java 9 or later, and doesn’t break existing users. The module name becomes effectively part of the API—once chosen, changing it constitutes a breaking change.
Many popular open-source libraries have added these entries, making them safer to use during migration. Projects that haven’t done so force their users to depend on unstable filename-derived names.
Real-World Migration Scenarios
Migrating to modules typically happens incrementally. In real-world enterprise applications, where frameworks like Spring, Hibernate, or older JDBC drivers are involved, automatic modules help ensure smooth incremental migration. The approach depends on whether you control the dependencies.
For applications where you own all the code, bottom-up migration works best. Start by converting leaf dependencies—libraries with no dependencies of their own—into proper modules. Then move up the dependency tree, converting each layer once its dependencies are modularized. Automatic modules serve as temporary placeholders for libraries not yet converted.
For applications using third-party dependencies, top-down migration makes more sense. Add a module descriptor to your application code first, declaring requirements on automatic modules for your dependencies. As those dependencies add proper module descriptors over time, you update your requirements to reference the explicit modules. This approach lets you start using the module system immediately without waiting for the entire ecosystem.
The challenge emerges with transitive dependencies. It’s not enough to set up all explicit dependencies to work with JPMS—all transitive dependencies must also be properly modularized. An application might depend on library A, which depends on library B, which depends on library C. If C lacks proper module support, the entire chain becomes problematic. Tracking down these deep dependencies and addressing them requires patience.
The Limitations and Pitfalls
Automatic modules provide compatibility, but they come with significant drawbacks that developers must understand. Automatic modules export all packages, breaking encapsulation; implicitly require all other modules, creating hidden dependencies; use module names based on JAR filenames that are unstable across versions; and overreliance delays proper modularization.
These aren’t just theoretical concerns. Exporting every package means internal implementation details become accessible, encouraging dependencies on code never meant to be public. The implicit dependency on all modules creates situations where changes to unrelated parts of the system can break things in surprising ways. Debugging becomes harder when the dependency graph isn’t explicit.
Features that make the module system compelling don’t work fully with automatic modules. jlink, which creates custom runtime images containing only the modules an application needs, cannot include automatic modules. One developer saw a 5.8x reduction (83%) in runtime image size after migrating from automatic modules to explicit modules with jlink. Organizations staying on automatic modules can’t achieve these optimizations.
There’s community concern that releasing modular JAR files depending on filename-derived automatic module names to Maven Central causes “Module Hell”—when transitive dependencies modularize with different names, users experience breakage. This has led to widespread advice: don’t publish modules that depend on filename-derived automatic modules to public repositories. Wait until dependencies have stable names through Automatic-Module-Name entries or full module descriptors.
When to Use Automatic Modules
Automatic modules serve specific purposes and shouldn’t be overused. They’re appropriate during active migration when you’re converting a codebase incrementally and need dependencies to participate in the module system. They work for internal applications where you control the update cycle and can coordinate changes across components. They’re acceptable for prototyping modular architectures before committing to full migration.
They’re inappropriate for libraries published to public repositories that other projects depend on. The instability of filename-derived names and the implicit dependencies create problems for consumers. They’re not suitable for new projects starting from scratch, which should use proper modules from the beginning. They shouldn’t be a permanent solution—automatic modules are like training wheels when learning to ride a bike; they help during transition, but keeping them forever prevents fully enjoying the efficiency and security of a modular system.
If you’re evaluating whether to use automatic modules, ask whether the dependency provides value during migration or creates long-term technical debt. If it’s truly temporary and enables progress toward full modularization, it’s probably worth it. If it’s a permanent workaround avoiding the work of proper migration, reconsider.
Moving Beyond Automatic Modules
The ultimate goal should always be explicit modules with proper module descriptors. These provide strong encapsulation by exporting only intended public APIs, express actual dependencies for reliable configuration, enable advanced features like custom runtime images, and improve security through better access control.
Converting an automatic module to an explicit module involves several steps. Analyze the code to determine which packages should be public APIs and which should remain internal. Create a module-info.java file declaring required dependencies explicitly. Test thoroughly to ensure nothing breaks when dependencies and exports become explicit. Update build configurations to work with the module path rather than classpath.
Tools can help with this process. The jdeps tool analyzes dependencies and can generate starter module descriptors. IDEs like IntelliJ IDEA provide module system support with warnings and quick fixes. Build tools like Maven and Gradle have evolved to handle modular projects smoothly.
The work is significant but worthwhile. Organizations that complete the migration report benefits in maintainability, security, and performance. The module system’s strong encapsulation prevents accidental dependencies and makes large codebases more manageable. But getting there requires treating automatic modules as temporary bridges, not permanent solutions.
What We’ve Learned
Automatic modules provide the critical bridge between legacy Java and the module system. They allow non-modular JARs to participate in modular applications by generating module metadata automatically, making incremental migration practical rather than requiring big-bang rewrites.
The mechanism works by placing regular JARs on the module path where they become automatic modules with all packages exported and implicit dependencies on everything available. Module names come from either a manifest entry or filename derivation, with the manifest approach providing stability. Library maintainers should add Automatic-Module-Name entries as a minimum step toward module system compatibility.
However, automatic modules have real limitations. They break encapsulation by exporting everything, create hidden dependencies through implicit requires, and don’t support advanced features like custom runtime images. They work best as temporary aids during migration, not as permanent solutions. Organizations using them should have clear plans to move toward explicit modules.
For teams migrating legacy applications, automatic modules make the journey manageable. Start by ensuring dependencies have stable names through manifest entries. Convert your own code incrementally, using automatic modules for dependencies not yet modularized. As the ecosystem matures and dependencies add proper module support, replace automatic modules with explicit ones.
The Java module system represents a significant evolution in how Java applications are structured. Automatic modules provide compatibility with the past while enabling progress toward the future. Understanding their role, benefits, and limitations helps teams navigate the transition successfully without getting stuck in permanent half-measures.


