Core Java

Automatic Modules: Bridging Legacy and Modular Java

When Java 9 dropped in 2017, it brought with it one of the most ambitious changes in Java’s history: the Java Platform Module System (JPMS), affectionately known as Project Jigsaw. After two decades of classpath chaos, Java finally had a real module system. But there was a problem—a massive one. What about the billions of lines of existing Java code? What about the thousands of libraries on Maven Central that had never heard of modules?

Enter automatic modules: the clever compromise that made JPMS migration feasible instead of catastrophic. They’re the bridge between yesterday’s classpath world and tomorrow’s modular future. Think of them as diplomatic translators at the border between two countries—not citizens of either, but essential for communication between both.

Let’s dive deep into how automatic modules work, why they matter, and how to use them effectively in your migration journey.

The Migration Challenge: Why Java Needed a Bridge

Imagine you’re managing a large enterprise application. You’ve got Spring, Hibernate, Apache Commons, Google Guava, and dozens of other dependencies. It’s 2017, and you want to migrate to Java 9’s module system. There’s just one problem: none of your dependencies have been modularized yet.

Without some kind of compatibility layer, the conversation goes like this:

You: “I’d like to use modules for my application.”
JPMS: “Great! Do all your dependencies have module descriptors?”
You: “No, they’re just regular JAR files.”
JPMS: “Then you can’t use them in a modular application.”
You: “So I can’t migrate?”
JPMS: “Correct.”
You: “…”

This would have forced a bottom-up migration: every library at the bottom of the dependency tree would need to modularize first before anything above it could. It would take years—maybe decades—for the entire ecosystem to migrate. The Java community would effectively be held hostage by the slowest-moving dependency.

The JDK comes with tools to help developers migrate existing code to JPMS. Application code can still have dependencies upon pre-Java-9 libraries, these jar files are treated as a special “automatic” modules, making it easier to migrate gradually to Java 9.

What Are Automatic Modules?

Automatic modules are plain JARs (no module descriptor) on the module path. Everything on the module path, regardless whether it’s a plain jar or a jar with a module-info, becomes a named module.

Here’s the magic: Automatic modules are JAR files without a module-info.java that behave like modules simply by being placed on the module path instead of the classpath.

Let’s break down what happens when you place a regular JAR file on the module path:

1. It Gets a Module Name

The module name of an automatic module is derived from the JAR file used to include the artifact if it has the attribute Automatic-Module-Name in its main manifest entry. The module name is otherwise derived from the name of the JAR file.

Example transformations:

  • commons-io-2.11.0.jar → module name: commons.io
  • jackson-databind-2.14.2.jar → module name: jackson.databind
  • my-awesome-library.jar → module name: my.awesome.library

The naming algorithm strips version numbers, replaces hyphens with dots, and removes invalid characters. But this filename-based naming is dangerous—more on that later.

2. It Exports Everything

Since there are no explicit exports/opens for packages residing in the automatic module, every package in an automatic module is considered to be exported even if it might actually be intended only for internal use.

Your careful internal-only packages? All public now. This breaks encapsulation, but it’s necessary for compatibility. Legacy code often relies on accessing “internal” packages, and automatic modules preserve that behavior.

3. It Requires Everything

No practical way to tell, in advance, which other modules an automatic module might depend upon. Automatic module receive special treatment during resolution so that they read all other modules in the configuration.

An automatic module implicitly requires every other module on the module path, plus everything on the classpath. This is the opposite of modular design, but again, it’s about compatibility. Legacy JARs weren’t explicit about dependencies, so JPMS assumes they need everything.

4. It Bridges the Classpath

Here’s the really clever part: Automatic modules allow the modulepath to depend on the classpath, something which is normally not allowed.

Regular modules can’t see anything on the classpath (the “unnamed module”). But automatic modules can. This lets modular code use automatic modules, which in turn can use old classpath JARs. It’s a three-way handshake connecting all three worlds.

A Practical Example: Migrating a Spring Application

Let’s walk through a real migration scenario. You have a Spring Boot application with these dependencies:

my-app (your code)
├── spring-boot-starter-web
├── spring-data-jpa
├── hibernate-core
├── postgresql-driver
└── apache-commons-lang

None of these libraries are modularized yet. Here’s your migration path:

Step 1: Create Your Module Descriptor

// module-info.java
module com.mycompany.myapp {
    requires spring.boot;  // Will be automatic module
    requires spring.data.jpa;  // Will be automatic module
    requires hibernate.core;  // Will be automatic module
    requires postgresql;  // Will be automatic module
    requires org.apache.commons.lang3;  // Will be automatic module
    
    // Your exports
    exports com.mycompany.myapp.api;
}

Step 2: Place Dependencies on Module Path

Configure your build tool to place JARs on the module path instead of classpath:

Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <release>17</release>
    </configuration>
</plugin>

Gradle:

java {
    modularity.inferModulePath = true
}

Step 3: Run Your Application

java --module-path mods --module com.mycompany.myapp/com.mycompany.myapp.Main

Your explicit module (com.mycompany.myapp) now depends on automatic modules (Spring, Hibernate, etc.), which can access anything they need. The migration is complete—at least for your application code.

The Automatic-Module-Name: Your Stability Anchor

Here’s where things get treacherous. When a module depends on an automatic module based on a filename, and that module is then depended on by others, the whole stack is linked. Everything in the stack has to go from v1 to v2 together.

This is what Stephen Colebourne (creator of Joda-Time) calls “Module Hell”.

The Problem with Filename-Based Names

Imagine this scenario:

  1. You depend on guava-30.0-jre.jar → automatic module name: guava.30.0.jre
  2. You release your library v1.0 with requires guava.30.0.jre;
  3. Guava releases v31.0 → JAR name changes to guava-31.0-jre.jar
  4. Module name changes to guava.31.0.jre
  5. Your v1.0 release is now broken because it requires a module name that no longer exists

The main piece of mitigation proposed for this problem is that jar files can have a new entry in MANIFEST.MF called “Automatic-Module-Name”. When JPMS examines an automatic module, if the MANIFEST.MF entry is present then it uses the value as the module name instead of the filename.

The Solution: Explicit Module Names

Library maintainers should add this to their JAR manifest:

Maven:

<plugin>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Automatic-Module-Name>com.google.common</Automatic-Module-Name>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Gradle:

tasks.jar {
    manifest {
        attributes["Automatic-Module-Name"] = "com.google.common"
    }
}

Now, regardless of the JAR filename, the module name is always com.google.common. The module name should be globally unique and composed of dot-separated Java identifiers. It should usually be a reversed domain name such as commonly found in Java package names.

This lets you:

  • Upgrade Guava from 30.0 to 31.0 without breaking dependent modules
  • Choose a name that matches your primary package
  • Reserve your module name for future explicit modularization

Real-World Example: RxJava

RxJava addressed this by specifying a stable automatic module name through an Automatic-Module-Name manifest entry: Automatic-Module-Name: io.reactivex, while still targeting JDK8.

This is brilliant: RxJava remained Java 8 compatible (no module-info.java) but prepared for JPMS by declaring its future module name. Users of RxJava could confidently write requires io.reactivex; knowing that name wouldn’t change when RxJava eventually became an explicit module.

Migration Strategies: Top-Down vs. Bottom-Up

The module system is designed to support both “bottom up” and “top down” migration. Bottom up migration means that you migrate your small utility libraries first, and your main applications last. Top down migration means that you migrate your application first, and the utility libraries later on.

Top-Down Migration (Recommended for Applications)

This is the pragmatic approach for application developers:

  1. Keep everything on classpath initially (unnamed module)
  2. Move application code to module path, add module-info.java
  3. Application reads all dependencies as automatic modules
  4. Gradually migrate internal libraries as time permits
  5. Wait for third-party libraries to become explicit modules naturally

Pros:

  • You control your application’s modularization timeline
  • Don’t wait for dependencies
  • Incremental progress is visible and valuable

Cons:

  • Temporary reliance on automatic modules
  • Must handle reflection-heavy frameworks carefully

Bottom-Up Migration (Ideal for Libraries)

In this strategy initially all JAR files that are part of the application will be located at its module path, so all non-migrated projects are treated as automatic modules. Choose the higher-level project in the dependencies hierarchy that has not yet been migrated to module, add the module-info.java file to the project to transform it from an automatic module to a named module.

Pros:

  • Proper modular architecture from bottom up
  • Each layer fully modularized before depending layers migrate

Cons:

  • Slow community-wide coordination required
  • Blocked by slowest dependencies

The Dark Side: Pitfalls and Limitations

Automatic modules aren’t perfect. Overreliance on them can lead to fragile systems, unexpected dependencies, and maintenance issues.

1. Broken Encapsulation

Everything is exported. Your carefully hidden internal APIs? Exposed. This can create dependencies on implementation details you didn’t intend to support.

// In automatic module "my.library"
package my.library.internal;  // You intended this as INTERNAL

public class SecretImplementation {
    // But because it's an automatic module, this is now PUBLIC API
    public void doSomethingSecret() { }
}

// In your application module
import my.library.internal.SecretImplementation;  // This works!

SecretImplementation.doSomethingSecret();  // Uh oh

When the library eventually becomes an explicit module and stops exporting my.library.internal, your code breaks.

2. Implicit Requires-All

Every automatic module requires every other module. This creates hidden transitive dependencies.

// Your module
requires my.library;  // which is automatic

// You now implicitly have access to:
// - Everything my.library directly depends on
// - Everything those dependencies depend on
// - Everything else on the module path

// This is "dependency hell" in disguise

3. Module Name Instability

Without Automatic-Module-Name in the manifest, upgrading a dependency can break your build:

# Build works
requires commons.io.2.11.0;

# Upgrade dependency
# Build breaks because module name changed!
requires commons.io.2.12.0;  # Now this is required

4. Split Package Nightmares

Split packages can be a challenge: Refactor or merge to avoid same package in multiple modules.

JPMS forbids two modules from owning the same package. If you have:

  • my.library.utils in JAR A (automatic module my.library.a)
  • my.library.utils in JAR B (automatic module my.library.b)

The module system refuses to load both. This was common in classpath-based systems but is illegal in JPMS.

Best Practices: Using Automatic Modules Wisely

Use automatic modules only as transitional aids, gradually replace them with explicit modular JARs, define stable names with Automatic-Module-Name in MANIFEST.MF, document usage in CI/CD pipelines, and keep track of transitive dependencies.

For Library Maintainers

1. Add Automatic-Module-Name immediately

Even if you’re not ready for full modularization:

tasks.jar {
    manifest {
        attributes["Automatic-Module-Name"] = "com.mycompany.mylib"
    }
}

This costs nothing but provides huge value to users.

2. Choose names carefully

The module name should have the same name as the root package of the JAR file. For example, if a JAR file contains com.google.utilities.i18n and com.google.utilities.strings then com.google.utilities is a good choice for module name.

Follow reverse-DNS naming: com.google.guava not guava.

3. Plan your eventual explicit module

The Automatic-Module-Name you choose now will be your module-info.java name later. Pick wisely.

For Application Developers

1. Track automatic module usage

Maintain a list of which dependencies are automatic modules:

// module-info.java
module com.myapp {
    // AUTOMATIC MODULES (to be replaced)
    requires spring.boot;  // TODO: Replace when Spring fully modularizes
    requires hibernate.core;  // TODO: Replace when Hibernate fully modularizes
    
    // EXPLICIT MODULES
    requires java.sql;
    requires java.logging;
}

2. Use jdeps to analyze dependencies

jdeps --module-path mods --check myapp.jar

# Shows:
# - Which modules are automatic
# - Split packages
# - Dependencies on JDK internals

3. Set migration milestones

  • Q1 2025: Application code modularized
  • Q2 2025: Internal libraries modularized
  • Q3 2025: Replace automatic modules with explicit where available

4. Handle reflection carefully

Frameworks like Spring and Hibernate use heavy reflection. You may need:

java --module-path mods \
     --add-opens com.myapp/com.myapp.entities=hibernate.core \
     --module com.myapp/com.myapp.Main

The --add-opens flag lets Hibernate reflectively access your entity classes even though they’re in a module.

The State of the Ecosystem (2025)

Five years after JPMS’ release, there are mixed thoughts about the JPMS. The benefits are obviously there – security, optimization, strong encapsulation. However, too much of the library ecosystem is behind, even 5 years later. The JPMS is a system in which the benefits shine when everyone plays along.

As of 2025, major libraries have made progress:

Fully Modularized:

  • Most JDK modules (java.base, java.sql, etc.)
  • Jackson (jackson-databind, jackson-core)
  • SLF4J and Logback
  • JUnit 5

Using Automatic-Module-Name:

  • Spring Framework (various modules)
  • Hibernate ORM
  • Apache Commons libraries
  • Google Guava

Still Problematic:

  • Some legacy JDBC drivers
  • Older Apache projects
  • Niche libraries stuck on Java 8

The good news: Most actively maintained libraries now at least define Automatic-Module-Name, making top-down migration viable.

When NOT to Use Modules

Not every project needs JPMS. It’s okay to skip modules if:

  • You’re building internal applications with no distribution concerns
  • Your dependencies don’t support it well
  • The migration cost outweighs benefits
  • You’re stuck on Java 8 for compatibility reasons

In Java 9 you can still use the -classpath argument to the Java VM when running an application. On the classpath you can include all your older Java classes, just like you have done before Java 9.

Java 9+ doesn’t force you to use modules. The classpath still works. Automatic modules are there if you want to bridge to modularity, not because you must migrate.

The Verdict: Training Wheels or Permanent Fixture?

Automatic modules are like training wheels when learning to ride a bike. They help you transition, but keeping them forever prevents you from fully enjoying the efficiency and security of a modular system.

This analogy is spot-on. Automatic modules were designed as a temporary bridge, not a destination. They sacrifice many benefits of proper modules:

  • No encapsulation (everything exported)
  • No explicit dependencies (requires everything)
  • Potential name instability (filename-based naming)

But they’re absolutely essential for migration. Without them, JPMS would have been DOA—impossible to adopt without coordinating thousands of open-source projects.

The Path Forward

Here’s the realistic timeline for most projects:

2017-2020: Automatic modules introduced, early adopters experiment
2020-2023: Major libraries add Automatic-Module-Name
2023-2025: Progressive conversion to explicit modules
2025-2030: Most active projects fully modularized
2030+: Automatic modules rare, used only for legacy dependencies

We’re in the middle phase now. If you’re starting a migration today:

  1. Your application: Can become an explicit module immediately
  2. Your libraries: Should have Automatic-Module-Name at minimum
  3. Your dependencies: Will mix automatic and explicit modules
  4. Your future: Gradually replace automatic with explicit

Code Example: Complete Migration Workflow

Let’s see a complete before-and-after migration:

Before (Classpath-based)

// src/com/myapp/Main.java
package com.myapp;

import com.google.common.collect.ImmutableList;
import org.apache.commons.lang3.StringUtils;

public class Main {
    public static void main(String[] args) {
        var list = ImmutableList.of("Hello", "World");
        System.out.println(StringUtils.join(list, " "));
    }
}
# Compile and run
javac -cp libs/guava-31.0.jar:libs/commons-lang3-3.12.jar \
      src/com/myapp/Main.java
      
java -cp .:libs/guava-31.0.jar:libs/commons-lang3-3.12.jar \
     com.myapp.Main

After (Module-based with Automatic Modules)

// src/module-info.java
module com.myapp {
    // Automatic modules (no module-info in these JARs)
    requires com.google.common;  // Guava with Automatic-Module-Name
    requires org.apache.commons.lang3;  // Commons Lang with Automatic-Module-Name
}

// src/com/myapp/Main.java
package com.myapp;

import com.google.common.collect.ImmutableList;
import org.apache.commons.lang3.StringUtils;

public class Main {
    public static void main(String[] args) {
        var list = ImmutableList.of("Hello", "World");
        System.out.println(StringUtils.join(list, " "));
    }
}
# Compile and run with modules
javac --module-path libs \
      -d mods/com.myapp \
      src/module-info.java src/com/myapp/Main.java
      
java --module-path mods:libs \
     --module com.myapp/com.myapp.Main

The code is identical; only the packaging changed. But now you have:

  • Explicit declaration of dependencies
  • Module boundary enforcement
  • Path toward full modularization
  • Ability to use jlink for custom runtime images

The jlink Bonus: Dramatic Size Reduction

Before migration, an equivalent runtime image created with launch4j weighed in at a hefty 175.1 MB. Using JPMS with jlink, the image was only 30.3 MB, a 5.8x reduction (83%)!

Once you’re fully modularized (no automatic modules remaining), you can use jlink to create custom JRE images containing only the modules you need:

jlink --module-path $JAVA_HOME/jmods:mods:libs \
      --add-modules com.myapp \
      --output myapp-runtime \
      --launcher myapp=com.myapp/com.myapp.Main

# Result: myapp-runtime/ is a self-contained JRE + your app
# Can be 70-80% smaller than bundling full JDK

This only works when all your dependencies are explicit modules. Automatic modules can’t be included in jlink images—one more reason they’re training wheels, not the end goal.

What We’ve Learned in This Article

Key Takeaways:

Automatic modules were introduced in Java 9 as a compatibility mechanism to solve a massive migration problem. When JPMS arrived in 2017, the Java ecosystem already contained billions of lines of code and thousands of libraries that were never designed with modularity in mind. Converting everything at once was unrealistic, so automatic modules provided a bridge that allowed legacy JARs to participate in the module system without requiring immediate changes. By placing a plain JAR on the module path, Java treats it as an automatic module, deriving its name from the filename, exporting all of its packages, and implicitly requiring every other module. This design sacrifices modularity’s guarantees—such as encapsulation and explicit dependencies—in order to preserve compatibility and allow gradual migration.

However, this filename-based naming system introduces serious instability. A simple version upgrade can change the JAR’s filename and thus its module name, breaking applications that depend on it. This led to the risk of “Module Hell,” where module resolution fails due to inconsistent or conflicting names. The recommended solution is to provide an Automatic-Module-Name manifest entry, which establishes a stable, future-proof name for the module without requiring a full module-info.java. Library maintainers therefore have a responsibility to at least declare this manifest attribute, as it costs nothing but provides enormous value to users and reserves the module name for future explicit modularization.

Automatic modules also play a crucial role in what can be described as a three-way bridge. They connect explicit modules, automatic modules, and the unnamed module on the classpath, enabling the coexistence of modular and non-modular code. This makes top-down migration—where application developers modularize their own code first—the most practical strategy for most teams, whereas bottom-up migration would require coordinated modularization across the entire ecosystem. Application developers can therefore treat automatic modules as temporary dependencies, track where they exist in their dependency graph, and replace them gradually as upstream libraries become fully modular.

Despite their usefulness, automatic modules come with significant downsides. They export all of their packages, which eliminates encapsulation, and they implicitly require all other modules, which hides true dependency relationships. They can also trigger split-package conflicts that were legal on the classpath but forbidden in the module system. Reflection-heavy frameworks such as Spring and Hibernate add more complexity, often requiring --add-opens or similar flags to access internal APIs, especially when mixing explicit and automatic modules.

As of 2025, the ecosystem has made substantial progress, with major libraries either fully modularized or at least declaring Automatic-Module-Name. Still, many mid-tier and long-tail libraries remain non-modular, so the migration is incomplete. This slows adoption of benefits such as jlink, which can create dramatically smaller and more efficient custom runtime images—but only when the entire dependency graph consists of explicit modules. Because automatic modules block this capability, they function like training wheels: intentionally temporary aids that help developers move toward full modularization but are not meant to be permanent.

Crucially, JPMS remains optional. The classpath continues to work, and many projects legitimately choose not to adopt modules at all. For projects that do aim to modularize, the current phase (2023–2025) is one of mixed dependencies, transitional tooling, and steady but gradual progress across the ecosystem. Ultimately, automatic modules represent a core trade-off: they give up the strictness, safety, and optimization opportunities of true modules in exchange for compatibility, incremental migration, and ecosystem stability. They are essential to the adoption of JPMS—but they also embody technical debt that must eventually be resolved.

The bottom line: Automatic modules transformed JPMS from a migration disaster into a feasible gradual transition. They’re not perfect—they break encapsulation and create hidden dependencies—but they’re absolutely necessary. Use them strategically as stepping stones toward full modularization, not as permanent solutions. For library maintainers, add Automatic-Module-Name today. For application developers, modularize your code now and replace automatic dependencies as the ecosystem matures.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button