Module Import Declarations Java 25 – JEP 511

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

Module Import Declarations Java 25 – JEP 511

Module Import Declarations Java 25 introduces a new way to simplify imports in modular Java applications. In this article, we explain JEP 511, why the import module feature was introduced, how it works, its rules and limitations, and when it should be used. Through clear before-and-after examples, you’ll see how this feature reduces import clutter while keeping Java’s module boundaries intact.

The Java Platform Module System introduced a clear and structured way to define dependencies at the application level. Modules allow us to group related packages, declare what an application requires, and control which APIs are exposed. Over time, this has helped teams build cleaner architectures and reason more confidently about large codebases.

However, while modules changed how Java applications are designed, they did not change how Java source files are written. Even in fully modular applications, developers still import individual classes and packages, just as they did before Java 9. This creates a small but persistent gap between modular design and everyday coding.

JEP 511, Module Import Declarations, addresses this gap. It introduces a way for Java source code to recognise modules directly, reducing import noise and improving readability when working with large modular APIs. The feature does not alter Java’s access rules or module boundaries. Instead, it builds on the existing module system, making modular code easier to write and maintain.

In this chapter, we explore why JEP 511 was needed, what it introduces, and when it should be used. Through simple examples, we will see how module imports align Java source code more closely with modular architecture, making modern Java applications cleaner and more expressive.

1. Why was JEP 511 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.

2. 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.

3. 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 statement makes the public API of the specified module available to the current source file. Rather than managing long and repetitive import lists, we can rely on the module’s definition to decide what is visible.

When a module is imported this way, the compiler:

  • Makes public types available from the module
  • Considers only packages that the module exports
  • Applies the module system’s access boundaries while resolving names

As a result, source files become cleaner and easier to read, while the module boundary remains intact.

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. The module import takes care of this and keeps the source file focused on its logic.

This convenience does not change Java’s access control model. The rules defined by the Java Platform Module System—such as encapsulation and exported packages—remain exactly the same.

What does change is how the compiler resolves imports when a module import is present.

The following 3 subsections describe the behaviour of import module, while still operating within the existing module rules.

3.1. 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.

3.2. 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 behaviour 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.

3.3. 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 modernise 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.

4. Importing modules in practice: before and after JEP 511

To understand how module import declarations work in practice, let us walk through a simple modular example and see how imports are written with and without JEP 511. The goal is to focus only on the difference in import style, not on application logic or architecture.

We will use a small application made up of two modules:

  • payments.core, which exposes payment-related services
  • payments.app, which uses those services

The modules and their behaviour remain the same throughout this section. Only the way imports are written inside the Java source file changes.

4.1. Before JEP 511: traditional imports

a. Create a new Java project

File → New Project → Java

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

b. Create payments.core module and module-info.java file

File → New → Module

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

Create file – module-info.java ( payments.core/src → New → module-info.java )

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

payments.core exposes two simple services, PaymentService and InvoiceService

package com.payments.core;

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

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

c. Create Module: payments.app and module-info.java file

File → New → Module

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

Create file – module-info.java ( payments.app/src → New → module-info.java )

The payments.app module declares its dependency on the core module:

module payments.app {
    requires payments.core;
}

Inside the application’s source file, each required class must still be imported explicitly:

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);
    }
}

d. 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

e. Run the application

  • Right-click MainApp
  • Click Run MainApp

Even though the module dependency is already declared, the source file must manage individual imports. As the number of public types grows, this import section becomes longer and harder to maintain. Running the application produces the expected output:

Output:
Payment of ₹500 processed
Invoice of ₹1000 processed

4.2. After JEP 511: a single module import

With JEP 511, the project structure remains exactly the same. The only change is in the import section of the source file.

Instead of importing individual classes, we import the module itself:

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);
    }
}

This single statement makes all public types exported by the payments.core module available to the source file. The code itself remains unchanged, but the import section becomes simpler and more expressive.

If this code is compiled using Java 21, the compiler reports a syntax error. This is expected because the import module Syntax is introduced by JEP 511 and is not recognised by earlier Java versions.

When compiled and run using Java 25 with preview features enabled, the application runs successfully and produces the same output as before:

Running the MainApp using Java 25:

Output:
Payment of ₹500 processed
Invoice of ₹1000 processed

From a runtime perspective, nothing changes. The improvement is purely at the source-code level.

4.3 Module imports with platform modules

Module imports are not limited to application-defined modules. They also work with platform modules such as java.base.

For example:

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

Here, instead of importing java.util.List and java.util.Date individually, we import the java.base module once. All public types exported by that module become available automatically.

5. Important rules and boundaries

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.

5.1. Only exported packages are imported

The 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
}

Types from non-exported packages remain inaccessible. Attempting to use them results in a compilation error. JEP 511 improves convenience—but module boundaries remain intact.

5.2. No access to hidden or restricted packages

A module import does not expose everything inside a module. If a package is not exported, or if it is exported only to specific modules, it remains inaccessible even when the module itself is imported.

Packages that are intentionally kept for internal use continue to stay hidden. Attempting to access types from such packages still results in a compilation error. The import module statement does not relax or bypass these restrictions in any way.

This behaviour is important for long-term maintenance. It prevents accidental use of internal APIs, preserves strong encapsulation, and allows modules to evolve safely without breaking consumers.

In short, JEP 511 respects every access rule already enforced by the Java Platform Module System.

5.3. Name conflicts are still our responsibility

Module imports do not resolve type name conflicts automatically. If two imported modules expose public classes with the same simple name, the compiler cannot infer which one to use.

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 by reverting to a specific class import:

import com.payments.core.Validator;

Module imports reduce boilerplate, but they do not remove the need for clarity.

5.4. Mixing module imports and regular imports is allowed

JEP 511 does not force an all-or-nothing approach. Module imports and traditional imports can be freely combined.

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.5. Not a Replacement for Good API Design

Finally, it is important to understand what JEP 511 is not trying to do. It does not flatten namespaces, it does not weaken encapsulation, and it does not remove the responsibility of designing clean and focused APIs.

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

6. When to use JEP 511 (and when not to)

An 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.

6.1. When should we use JEP 511?

a. 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.

b. 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.

c. 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.

d. 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 modernise the import file by file.

6.2. When should we avoid JEP 511?

a. 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.

b. 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.

c. 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.

d. 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.

7. Java version support and final takeaways

Before wrapping up this chapter, it is useful to understand how JEP 511 fits into the Java release timeline and what it means in practice. This section clarifies which Java versions support module import declarations, what remains unchanged, and the key takeaways for everyday development.

7.1. Java version support

Module import declarations are a language-level feature, so their availability depends on the Java version being used.

The feature first appeared as a preview in Java 23 under JEP 476 and was refined in Java 24 through JEP 494, based on early feedback. With Java 25, the feature is finalised as JEP 511 without further changes. From Java 25 onward, import module is part of the standard Java language.

As a result, the source code that uses the import module behaves as follows. It compiles successfully in Java 25 and later versions. In Java 23 and 24, it requires preview support. In Java 21 and earlier releases, the syntax is not recognised, and compilation fails.

To use the final version of this feature, both the Java compiler and the IDE must be configured to run on Java 25 or newer.

7.2. What does not change

It is equally important to understand what JEP 511 does not change.

JEP 511 does not introduce Java modules. Modules have existed since Java 9 as part of the Java Platform Module System. It also does not weaken encapsulation or bypass module-info.java. All module boundaries, export rules, and access checks continue to work exactly as before.

The feature only affects how imports are written in source code. It does not alter how modules are resolved, loaded, or enforced at runtime.

7.3. Practical impact and final takeaway

What JEP 511 improves is the source-level experience. It allows import statements to express module-level dependencies directly, instead of forcing developers to work only at the package or class level. This makes source files easier to read, especially when working with large modular APIs.

From a practical point of view, JEP 511 is about developer ergonomics rather than architectural change. It reduces repetitive import statements, aligns everyday coding with modular design, and still allows traditional imports where precision or clarity is required. It does not remove the classpath, and it does not force modularisation.

The key takeaway is simple. For years, Java developers have designed systems using modules while still writing imports as if modules did not exist. With import module, Java finally allows source code to reflect that intent clearly. We depend on a module’s public API, not just a collection of packages.

In short, JEP 511 is a small syntactic change with a meaningful impact. It makes modular Java code cleaner, more readable, and easier to maintain—without breaking existing concepts or workflows.

8. 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.

Further Reading

For the official specification and design rationale behind module import declarations, refer to:

What’s Next

Module import declarations reduce import clutter and make modular code easier to read and maintain, especially in applications that depend on large, well-defined modules.

In the next chapter, we move toward making Java programs simpler to write and easier to get started with.

Compact Source Files and Instance Main Methods (JEP 512) introduces a more concise way to write small Java programs by reducing ceremony around class declarations and the main method, while remaining fully compatible with existing Java code.

Leave a Comment