JEP 511 Explained: Module Import Declarations in Java 25

  • Last Updated: January 2, 2026
  • By: javahandson
  • Series
img

JEP 511 Explained: Module Import Declarations in Java 25

JEP 511 Explained: Module Import Declarations in Java 25 — Learn why this feature was introduced, how import module works, its rules and limitations, real-world before-and-after examples, and when to use it effectively to simplify imports in modular Java applications.

Why JEP 511 was needed?

When Java modules were introduced in Java 9, it was a big step forward. Java finally got a strong way to group related packages together, clearly define dependencies, and hide internal APIs using the Java Platform Module System (JPMS).

At the module level, Java changed a lot. But at the code level, one thing stayed exactly the same. Even after Java 9, we still import packages, not modules.

import com.payments.core.PaymentService;
import com.payments.core.InvoiceService;
import com.payments.core.ReceiptService;
import com.payments.core.TaxService;

Now imagine payments.core is a well-designed module exposing dozens of public classes. Our payments.app module-info.java already declares:

requires payments.core;

So the compiler knows our application depends on that module. Yet in our Java files, we must still:

  • Import every class individually
  • Repeat the same package name again and again
  • Manually manage long import lists

This creates a clear gap between JPMS and everyday coding.

For modular APIs, this quickly becomes painful:

  • Large import sections grow noisy
  • Reading code becomes harder
  • Refactoring packages means touching many files
  • The benefit of “modules” stops at module-info.java

In short, Modules exist, but imports never learned about them.

Java developers ended up thinking: “If I already depend on a module, why am I still importing everything package by package?”

This is not a theoretical issue—it shows up immediately when working with:

  • Layered applications
  • Domain-driven modular code
  • Platform or framework-style modules

So the goal of JEP 511 is simple but powerful: Let Java source code acknowledge modules directly — not just packages. By doing so, Java finally aligns how we design systems with how we write code. And once we see this problem clearly, JEP 511 immediately feels… obvious.

Quick Recap: What are Java modules?

A Java module is simply a group of related packages that are treated as a single unit. Instead of exposing everything by default (like the old classpath), a module explicitly declares what it needs and what it allows others to use. This makes large applications more reliable, secure, and easier to maintain.

Think of a module as a well-defined boundary around your code. Other modules can only access what we intentionally expose—nothing more.

module-info.java – Every module is described using a special file called module-info.java. This file sits at the root of the module and acts like a contract.

module payments.core {
    exports com.payments.core;
    requires java.sql;
}

requires → declares dependencies on other modules
exports → exposes specific packages to the outside world

Anything not exported remains hidden, even if the classes are public.

Java modules have been part of Java since Java 9. They are not new.

JEP 511 does NOT introduce modules. It simply makes working with existing modules simpler and cleaner at the source-code level.

What exactly does JEP 511 introduce?

JEP 511 introduces a new import form that allows us to import an entire module, instead of importing classes package by package.

import module java.base;

This single line changes how we think about imports in modular Java.

When we write the above line, we are explicitly telling the compiler:

  • Make all public types available
  • From all packages that the module exports
  • While respecting the module’s encapsulation rules
  • And include any transitively required modules as well

As a result, every public class that the module intentionally exposes becomes directly usable in our code—without writing dozens of individual import statements. This keeps the source file focused on business logic, not repetitive import management, while still honoring the strong boundaries defined by the module system.

For example, after importing java.base, we can directly use:

String s = "Java HandsOn";
List<Integer> list = List.of(1, 2, 3);

without explicitly importing java.lang.String, java.util.List, or related packages.

Export rules still apply

It’s important to note what does not change:

  • Only exported packages are included
  • Non-exported (internal) packages remain inaccessible
  • Encapsulation rules defined by the module are still enforced

So this is not a wildcard free-for-all. The module’s boundaries remain fully intact.

Transitive dependencies are included

One important detail of the import module is that it fully respects transitive dependencies defined by the module system.

If a module declares a dependency like this:

module payments.core {
    exports com.payments.core;
    requires transitive payments.common;
}

And we write:

import module payments.core;

Then we automatically gain access to:

  • All public types of payments.core
  • And all public types exported by payments.common

There is no need to import the transitive module separately. This behavior mirrors how JPMS already resolves dependencies at runtime—JEP 511 simply brings that same clarity to source-level imports.

In short, module relationships defined in module-info.java now directly influence what we can use in our code, without extra ceremony.

Works on both classpath and module path

Another key strength of JEP 511 is that it works in both modular and non-modular environments.

Module path — for fully modular applications using JPMS

Classpath — for traditional applications without module-info.java

This means we can write: import module java.base;

even in a classic classpath-based project, and the compiler will still resolve the module correctly.

There is no forced migration, no “all-or-nothing” adoption. We can start using module imports incrementally, file by file, without restructuring the entire application.

By supporting both classpath and module path:

  • Existing codebases can adopt JEP 511 safely
  • Modular APIs become easier to consume
  • Teams can modernize imports without rewriting architecture

JEP 511 is intentionally designed as a source-level improvement, not a breaking change—making it practical, low-risk, and easy to adopt.

Before vs After: Code Examples

Without JEP 511 (multiple imports)

1. Create a new Java project

File → New Project → Java

  • Uncheck Maven / Gradle
  • Select JDK (17 / 21 — any is fine)
  • Finish

2. Create Module: payments.core

File → New → Module

  • Type: Java
  • Module name: payments.core
  • Finish

Create file – module-info.java

Right-click payments.core/src → New → module-info.java

module payments.core {
    exports com.payments.core;
}

Create class – payments.core/src/com/payments/core/PaymentService.java

package com.payments.core;

public class PaymentService {
    public void pay(int amount) {
        System.out.println("Payment of ₹" + amount + " processed");
    }
}

Create class – payments.core/src/com/payments/core/InvoiceService.java

package com.payments.core;

public class InvoiceService {
    public void invoice(int amount) {
        System.out.println("Invoice of ₹" + amount + " processed");
    }
}

3. Create Module: payments.app that requires payments.core

File → New → Module

  • Type: Java
  • Module name: payments.app
  • Finish

Create file – module-info.java

Right-click payments.app/src → New → module-info.java

module payments.app {
    requires payments.core;
}

Create Main class – payments.app/src/com/payments/app/MainApp.java

package com.payments.app;

import com.payments.core.InvoiceService;
import com.payments.core.PaymentService;

public class MainApp {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();
        paymentService.pay(500);

        InvoiceService invoiceService = new InvoiceService();
        invoiceService.invoice(1000);
    }
}

4. Add module dependency if using IntelliJ

File → Project Structure → Modules

  • Select payments.app
  • Open Dependencies tab
  • Click +
  • Choose Module Dependency
  • Select payments.core
  • Apply → OK

IntelliJ now knows: payments.app requires payments.core

5. Run the application

  • Right-click MainApp
  • Click Run MainApp
Output:
"C:\Program Files\Java\jdk-21\bin\java.exe" --enable-preview "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=52551" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -p C:\my-space\out\production\payments.app;C:\my-space\out\production\payments.core -m payments.app/com.payments.app.MainApp
Payment of ₹500 processed
Invoice of ₹1000 processed

With JEP 511, single import

We will keep everything as it is in the above project; we will just import the module in MainApp instead of multiple imports.

package com.payments.app;

import module payments.core;

public class MainApp {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();
        paymentService.pay(500);

        InvoiceService invoiceService = new InvoiceService();
        invoiceService.invoice(1000);
    }
}

Running the MainApp using Java 21, we got an error like: ‘;’ expected

import module payments.core;

This syntax comes from JEP 511: Module Import Declarations, which is:

  • Introduced in Java 25
  • Not present in Java 21
  • Not a preview in Java 21

So Java 21’s compiler simply does not understand import module.

Running the MainApp using Java 25:

"C:\Program Files\Java\jdk-25\bin\java.exe" --enable-preview "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=65161" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -p C:\my-space\out\production\payments.app;C:\my-space\out\production\payments.core -m payments.app/com.payments.app.MainApp
Payment of ₹500 processed
Invoice of ₹1000 processed

The same applies to platform modules like java.base as well.

package com.payments.app;

import module java.base;

public class MainApp {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3);
        System.out.println(numbers);

        Date date = new Date();
        System.out.println(date);
    }
}
Output:
[1, 2, 3]
Tue Dec 30 12:45:23 IST 2025

In the above example, instead of importing java.util.Date and java.util.List we have just imported the java.base module.

Important rules, limitations, and name conflicts

While the import module makes imports significantly cleaner, it follows strict and well-defined rules. Understanding these boundaries is important so that we use JEP 511 correctly—and avoid surprises.

1. Only exported packages are imported – import module does not bypass Java’s encapsulation rules.

When we write:

import module payments.core;

The compiler makes available only those packages that the module explicitly exports.

module payments.core {
    exports com.payments.core.api;
    // com.payments.core.internal is NOT exported
}

Trying to use a type from a non-exported package will still fail at compile time:

// Compilation error
InternalValidator validator = new InternalValidator();

JEP 511 improves convenience—but module boundaries remain intact.

2. No access to split or hidden packages

If a package is:

  • Not exported
  • Exported only to specific modules
  • Or intentionally hidden for internal use

then import module will not expose it.

This ensures:

  • Strong encapsulation
  • No accidental use of internal APIs
  • Safe evolution of modules over time

In short, JEP 511 respects every rule already enforced by JPMS.

3. Name conflicts are still our responsibility – One important limitation is type name ambiguity. If two imported modules expose a public class with the same simple name, the compiler cannot guess which one we mean.

import module payments.core;
import module payments.common;

Both modules export a class named Validator.

Validator validator = new Validator(); // Ambiguous reference

In such cases, we must resolve the conflict explicitly:

com.payments.core.Validator coreValidator = new com.payments.core.Validator();

or revert to a specific class import:

import com.payments.core.Validator;

import module reduces imports—but it does not remove the need for clarity.

4. Mixing module imports and regular imports is allowed – We are free to combine both styles when needed

import module payments.core;
import com.external.audit.AuditService;

This flexibility allows us to:

  • Use module imports for large, stable APIs
  • Use class imports where precision matters
  • Gradually adopt JEP 511 without rewriting everything

5. Not a Replacement for Good API Design – Finally, it’s important to understand what JEP 511 is not trying to do.

  • It does not flatten namespaces
  • It does not ignore encapsulation
  • It does not eliminate design responsibility

Well-designed modules with clean exports remain essential. JEP 511 simply removes unnecessary boilerplate, not architectural discipline.

When we should (and should not) use JEP 511?

import module is a powerful convenience feature—but like any tool, it works best when used in the right places. Below are clear guidelines to help us decide when to use JEP 511 and when to avoid it.

When should we use JEP 511?

1. When consuming large, stable modular APIs – If a module exposes many public types and is designed as a cohesive API, module imports greatly reduce noise.

import module payments.core;

This works especially well for:

  • Platform modules (java.base, java.sql)
  • Internal domain modules
  • Well-versioned framework-style modules

It keeps source files clean and focused on logic, not imports.

2. When most of a module’s public API is used – If we end up importing many classes from the same module anyway, a module import is more readable.

Before

import com.payments.core.PaymentService;
import com.payments.core.InvoiceService;
import com.payments.core.ReceiptService;

After

import module payments.core;

This clearly communicates intent: we depend on this module’s API.

3. In layered or modular architectures – In applications built around clear layers (domain, service, infrastructure), module imports align well with the design.

import module domain.core;
import module domain.shared;

This mirrors architectural boundaries directly in source code, making dependencies easier to reason about.

4. When migrating existing code gradually – JEP 511 works on both the classpath and module path, allowing us to adopt it incrementally.

import module java.base;
import com.external.logging.Logger;

There is no forced migration. We can modernize the import file by file.

When should we avoid JEP 511?

1. When name conflicts are likely – If multiple modules expose types with the same simple name, module imports can cause ambiguity.

import module payments.core;
import module payments.common;
Validator v = new Validator(); // ambiguous

In such cases, explicit imports improve clarity.

2. When only one or two classes are needed – If we use just a single class from a module, importing the entire module may be unnecessary.

import com.payments.core.CurrencyConverter;

This keeps dependencies precise and self-documenting.

3. For small or poorly designed modules – If a module:

  • Exports too many unrelated packages
  • Has a broad, unfocused API surface

then the import module can hide poor design rather than improve readability. Good module design still matters.

4. When explicitness is more important than brevity – In critical or security-sensitive code, being explicit about exactly which classes are used can be valuable. In such cases, traditional imports remain a better choice.

JEP 511 is best seen as a clarity and productivity feature, not a replacement for careful design. Use it when:

  • A module represents a clear, intentional API
  • Boilerplate imports are getting in the way

Avoid it when:

  • Precision and explicitness matter more than convenience

Used thoughtfully, JEP 511 makes modular Java cleaner, clearer, and easier to work with.

Java version support and final takeaways

JEP 511 is a language-level enhancement, which means its availability depends strictly on the Java version we are using. This feature is targeted for Java 25 and is not available in Java 21 or any earlier release. As a result, any source code that uses the new import module syntax will fail to compile on older Java versions. To use JEP 511, both the compiler and the IDE must be configured to use Java 25 or newer. There is no preview flag or backward compatibility mode for earlier versions, so this is a conscious opt-in that comes with upgrading the Java version.

It is also important to understand what JEP 511 changes—and what it intentionally leaves untouched. JEP 511 does not introduce Java modules; they have existed since Java 9 as part of the Java Platform Module System. It also does not weaken encapsulation rules or bypass module-info.java. All existing module boundaries, export rules, and access checks remain exactly the same. What JEP 511 improves is the source-level experience by allowing our import statements to reflect module-level dependencies instead of forcing us to work purely at the package level.

From a practical standpoint, JEP 511 is about developer ergonomics, not architectural change. It reduces repetitive import statements, makes module usage explicit inside Java source files, and aligns everyday coding with how modular systems are already designed. At the same time, it does not remove the classpath, does not force modularization, and does not prevent us from using traditional imports where clarity or precision is needed.

The key takeaway is that JEP 511 closes a long-standing gap in Java. For years, we have been designing systems using modules while still writing imports as if modules did not exist. With the import module, Java finally allows our code to express intent more clearly: we are depending on a module’s public API, not just a handful of packages. This makes modular Java cleaner, more readable, and easier to maintain—without breaking existing concepts or workflows.

In short, JEP 511 is a small syntactic change with a meaningful impact, bringing Java’s source code closer to the way modern Java applications are structured.

Conclusion

JEP 511 may look like a small syntax addition, but it solves a problem Java developers have quietly lived with since Java 9. While modules gave us strong boundaries, clear dependencies, and better encapsulation, our everyday coding experience remained stuck at the package-import level. That mismatch was real, and over time, it made modular code feel more verbose than it needed to be.

By introducing an import module, Java finally allows source code to express intent at the same level as system design. Instead of managing long lists of repetitive imports, we can now clearly state that our code depends on a module’s public API—nothing more, nothing less. All existing rules around exports, encapsulation, and access control continue to apply, so safety and structure are never compromised.

Most importantly, JEP 511 is practical. It works on both the classpath and the module path, can be adopted incrementally, and does not force architectural rewrites. We can use it where it adds clarity and avoid it where explicit imports make more sense.

In essence, JEP 511 doesn’t change how Java modules work—it changes how pleasant they are to use. It closes a long-standing gap between JPMS and everyday Java coding, making modular Java cleaner, more expressive, and easier to maintain.

Leave a Comment