View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All
View All

Introduction to Java Modules: A Paradigm Shift in Application Development

By Rohan Vats

Updated on May 28, 2025 | 21 min read | 18.28K+ views

Share:

Ever wonder how leading tech companies streamline their massive Java-based systems to ensure crystal-clear dependencies and unparalleled reliability? Most of the answers lie in Java Modules, introduced in Java 9. Their impact extends beyond code organization as Java modules fundamentally restructure application construction for a more robust and predictable outcome.

Java Modules, introduced in Java 9, offer a powerful solution to the challenges of managing complex Java applications. They resolve "JAR Hell" by enforcing strict module boundaries and explicit dependencies, unlike the loose dependency model of traditional JARs. 

This fundamental shift enhances reliability by preventing accidental access to internal APIs, improves scalability through more efficient memory usage and faster startup times, and strengthens security by allowing for fine-grained control over what code is exposed. The result is more maintainable and predictable systems that are easier to build and deploy.

This blog explores the core concepts behind Java 9 Module construction and runtime, guiding you through the practical steps of building and launching modular Java projects. 

Don't just learn Java Modules, truly excel at building robust, predictable software. Explore Online Software Development Courses from top universities to unlock your full potential. Master the most in-demand technologies and languages that leading companies use, ensuring your skills are current and highly marketable. Enroll today and transform your career as a full-stack engineer.

Java Modules: Core Concepts of Java Module System 

This section introduces the fundamental building blocks of the Java Platform Module System (JPMS). We'll explain what a module truly is and elaborate on the key declarations and mechanisms that govern its behavior. 

What are Java Modules?

At its core, a Java module is a named, self-describing collection of code (packages and classes) and data (resources) that explicitly declares its dependencies on other modules and defines which packages are accessible to other modules. This structure represents a significant evolution beyond traditional JARs, which often suffered from weak encapsulation and ambiguous dependencies.

This strong encapsulation and explicit dependency management are central to overcoming the classpath issues and promoting more reliable and maintainable applications. During runtime, the Java Virtual Machine (JVM) uses these declarations to construct a module graph, rigorously verifying all dependencies and failing fast if any are missing or unsatisfiable.

Are your Java applications growing increasingly complex? upGrad's Software and Tech Courses go beyond basic modularity, empowering you to architect robust, future-proof solutions. Master practical, industry-ready skills designed to tackle real-world challenges with these courses: 

Key Concepts of the Java Platform Module System (JPMS)

Let us now learn about the fundamental declarations and mechanisms that define a module's behavior and interactions within the Java ecosystem.

1. Module Declaration (module-info.java): The heart of any Java module is its module descriptor file, module-info.java. This file, located in the module's root directory, acts as its manifest, defining its name, readability, and accessibility rules.

// Example module-info.java
module com.example.mymodule {
    // Module declarations go here
}

2. Module Name: Every module must have a unique name. This name is crucial for the module system to identify and resolve dependencies. Best practice suggests using a reverse-domain name convention (e.g., com.example.mymodule) to ensure uniqueness and avoid conflicts.

3 . Requiring Dependencies (requires): Java 9 modules explicitly declare their dependencies on other modules using the requires keyword within module-info.java. This makes the module system aware of the module graph, ensuring all necessary modules are present at runtime.

module com.example.mymodule {
    requires java.base; // Declares a dependency on the base Java module
    requires com.example.anothermodule; // Depends on a custom module
}

Also Read: 15 Essential Java Full Stack Developer Skills in 2025

  • requires transitive: This is a crucial modifier for requires. If module A requires transitive module B, any module that reads module A will also implicitly read module B. This is essential when module A exposes types from module B in its public API (e.g., a method in A's exported package returns a type from B). Without transitive, modules depending on A would also have to require B, leading to redundancy explicitly.
  • requires static: This is less common but important for optional dependencies. requires static means the dependency is needed at compile time, but is optional at runtime. If the module isn't present at runtime, the application should gracefully handle its absence.

Example: Logging libraries or test frameworks may be required during development but not shipped in production.

4. Exporting and Opening Packages (exportsopens): All packages within a module are encapsulated by default. To make packages accessible to other modules, they must be explicitly exports or opens.

  • exports: Allows other modules to access the public types within the specified package at compile-time and runtime.
  • opens: Primarily used for reflective access at runtime. It allows other modules to use reflection (e.g., Class.forName()setAccessible(true)) to access all members (public, protected, private) within the specified package. Unlike exports, which support compile-time access, opens is for runtime reflection and does not expose types for compilation.

Coverage of AWS, Microsoft Azure and GCP services

Certification8 Months

Job-Linked Program

Bootcamp36 Weeks
module com.example.mymodule {
    exports com.example.mymodule.api; // Public API for other modules
    opens com.example.mymodule.internal; // Allows reflection on internal package
}

5. Qualified Exports/Opens (exports ... toopens ... to): Sometimes, you don't want to export a package to all modules but only to specific ones. Qualified exports/opens allow you to specify a comma-separated list of target module names. This provides fine-grained control over visibility, preventing unintended dependencies on internal APIs. Use this when exposing internal APIs to trusted plugins without polluting the public module interface.

module com.example.core {
    exports com.example.core.internal.spi to com.example.plugin.loader; // Only plugin.loader can access
}

Also Read: Why is Java Platform Independent Language-A Detailed Guide

6. Using and Providing Services (usesprovides): The service mechanism in JPMS facilitates the creation of pluggable architectures where modules can use (discover) implementations of a service interface and provide (implement) a service.

  • uses: Declares that a module may use a service defined by a given interface or abstract class. The module system can then locate implementations of this service via java.util.ServiceLoader.
  • provides ... with: Declares that a module implements a specific service interface.
// Module com.example.serviceuser
module com.example.serviceuser {
    uses com.example.service.MyService; // Declares intent to use MyService
}

// Module com.example.serviceprovider
module com.example.serviceprovider {
    provides com.example.service.MyService with com.example.serviceprovider.MyServiceImpl; // Provides an implementation
}

Other Important Considerations:

  • Automatic Modules: When you place a traditional JAR file (without a module-info.java) on the module path, the Java Module System treats it as an "automatic module."
    • Its name is derived from the JAR file name (or Automatic-Module-Name in MANIFEST.MF).
    • It implicitly exports all its packages.
    • It implicitly reads all other modules (both named and other automatic modules) on the module path.
    • While convenient for migrating legacy JARs, relying heavily on automatic modules can weaken the benefits of strong encapsulation and explicit dependencies. They may also introduce split-package errors if the same package exists in multiple automatic modules.
  • Unnamed Module: Any classes loaded from the traditional classpath (not the module path) belong to the "unnamed module."
    • The unnamed module automatically exports all its packages.
    • It implicitly reads all other modules (named and automatic).
    • However, named modules cannot require the unnamed module. This mechanism primarily exists for backward compatibility, allowing legacy code to run on a modular JVM. This enforces modular integrity. Legacy code should be gradually migrated to named modules to participate fully in JPMS.

To build truly intelligent and data-backed applications, you also need to enhance your analytical toolkit.  Explore our Hypothesis Testing Crash Course – master A/B testing and make data-driven decisions. Elevate your entire skill set today!

Also Read: 50 Java Projects With Source Code in 2025: From Beginner to Advanced

With a solid understanding of Java 9 Modules, the next logical step is to see them in action. We'll now walk through Building and Launching Modular Java Programs.

Building and Launching Modular Java Programs

You've learned the theory behind Java Modules; now let's get practical. This section cuts through the complexity, showing you exactly how to compile, package, and run your modular Java applications. We'll understand command-line tools and the different steps involved, turning concepts into actionable knowledge so you can confidently bring your modular projects to life.

Java 9 Module System

The Java 9 Module System, or Project Jigsaw, was a monumental release that fundamentally re-architected the Java platform into interconnected modules. This overhaul brought strong encapsulation and explicit dependencies, significantly improving security and performance by enabling leaner, more focused application runtimes.

It specifically aimed to dismantle long-standing issues like "JAR Hell" and circumvent the "classpath dilemma," allowing developers to build more reliable, scalable, and maintainable applications. Java Platform Module System offers a blueprint for robust software architecture by self-contained components and clearly defining their interactions.

While Java Modules revolutionize application architecture, the future of development is increasingly intelligent. Equip yourself with the Generative AI Mastery Certificate for Data Analysis for tomorrow's challenges. Learn to integrate AI into your robust datasets. Start building truly smart solutions today!

Also Read: Top 32 Exception Handling Interview Questions and Answers in 2025 [For Freshers & Experienced]

You've explored the "why" behind Java Modules; now let's dive into the "how." Before we compile and run, we'll set up a simple modular project, understanding the essential file structure and module-info.java declarations that make your application modular.

How to Create a Module in Java? - Compilation of Java Module

Here, you'll find step-by-step instructions on creating your module-info.java file and compiling your modular Java source code. This will demonstrate the practical application of the exports and requires directives, culminating in a ready-to-run modular JAR.

To create a Java module, you define its module-info.java descriptor and compile your source code with the Java compiler, javac, specifically instructing it about your module.

Step 1: Project Structure:  Organize your source code into a directory structure that reflects your module. Each module should have its root directory, and inside that, typically a src folder containing your module-info.java and package directories.

my-modular-project/
├── my.module.name/
│ ├── src/
│ │ ├── module-info.java
│ │ └── com/
│ │ └── example/
│ │ └── MyClass.java
└── out/

 

Step 2: Create module-info.java  Inside your module's src directory, create the module-info.java file. This is where you declare your module's name, its dependencies, and which packages it exports.

 

// my.module.name/src/module-info.java
module my.module.name {
    // We export com.example so other modules can use its public classes
    exports com.example;
    // If MyClass needed types from java.base (like String, System), we'd require it
    // requires java.base; //
}

Explanation:

  • module my. module.name { ... }: Defines a module named my.module.name.
  • exports com.example;: Specifies that the com.example package within this module is accessible to other modules that require my.module.name. Any classes not in an exported package remain strongly encapsulated.

Step 3: Write Your Java Code:  Create your regular Java source files within their respective packages.

// my.module.name/src/com/example/MyClass.java
package com.example;

public class MyClass {
    public String getGreeting() {
        return "Hello from MyClass in a module!";
    }
}

 

Step 4: Compile the Module:  Use the javac command. The key difference from traditional compilation is the use of the --module-source-path option to tell javac where to find your module source files, and --module to specify the module you are compiling. This creates compiled class files in a designated output directory.

# Navigate to the project root: my-modular-project/
mkdir -p out/my.module.name # Create output directory for the module

javac --module-source-path my.module.name/src \
      -d out \
      my.module.name/src/module-info.java \
      my.module.name/src/com/example/MyClass.java

Explanation:

  • --module-source-path my.module.name/src: Tells javac to look for module source roots in my.module.name/src.
  • -d out: Specifies out as the destination directory for compiled classes. javac will automatically create the my.module.name subdirectory inside out for the compiled module.

Note: javac places the output in a directory matching the module name under ‘out’ automatically.

  • my.module.name/src/module-info.java my.module.name/src/com/example/MyClass.java: The specific source files to compile. If compiling multiple modules, each should follow the directory structure rootdir/module.name/src.

After compilation, your out directory will contain the compiled module:

out/ 
├── my.module.name/
│ ├── module-info.class
│ └── com/
│ └── example/
│ └── MyClass.class

While Java Modules streamline advanced application development, every powerful solution starts with strong foundational coding. Build your core programming skills with our Learn Basic Python Programming course. Master Python, Matplotlib, and coding fundamentals in just 5 hours! Join 43k+ learners and kickstart your coding journey today!

You've successfully compiled your first modular Java code. To truly understand the power of the Java Module System, we'll now explore the prominent Java Module Construction and Runtime concepts.

Exploring Top 5 Advanced Concepts for Java Module Construction and Runtime

This section will shift from the detailed syntax of module declarations to modular Java's broader architectural and runtime aspects. We'll explore the fundamental mechanisms that enable modular applications to be packaged, discovered, and executed, focusing on how modules interact with the Java Virtual Machine and each other during the build and launch phases. 

Specifically, we'll delve into the module path, module resolution, and the creation of custom runtime images using jlink, giving you the tools to optimize deployment and ensure robust application execution.

1. Modular JARs

Modular JARs are essentially regular .jar files with a twist: they contain a compiled module-info.class file at their root. This file, derived from your module-info.java source, acts as a module descriptor, providing the Java runtime with all the necessary information about the module – its name, dependencies, and exported packages. Crucially, the module name defined in module-info.class must match the actual module name used for resolution on the module path.

When the JVM encounters a JAR with this descriptor on the module path, it treats it as a well-defined module, adhering to strong encapsulation and explicit dependency rules. This packaging ensures that modular applications are compact and efficient, as only the necessary components are included.

2. Module Path vs. Classpath

The distinction between the module path and the classpath is fundamental in modular Java.

  • Classpath: This is the legacy mechanism Java has used for decades to locate classes and resources. It's a flat list of JARs and directories. The classpath offers weak encapsulation; all JARs on it are effectively "friends," and name collisions (like "JAR Hell") are common. Code on the classpath operates within the unnamed module, which implicitly exports all its packages and reads all other modules (named or automatic). This leniency often leads to runtime issues due to missing or conflicting dependencies.
  • Module Path: Introduced with Java 9, the module path is specifically designed for the Java Module System. When you place a JAR on the module path, it's either a fully defined named module (with a module-info.class) or an automatic module (a regular JAR treated as a module). The module path enforces strict rules:
    • Strong Encapsulation: Only explicitly exported packages are visible to other modules.
    • Explicit Dependencies: Modules must declare what they requires.
    • No Split Packages: A package cannot exist in multiple modules.
    • Module Identity: Each module has a unique name.

The module path ensures that the JPMS can build a reliable and predictable module graph at startup, significantly reducing runtime errors caused by classpath issues.

Feature Classpath (Legacy) Module Path (JPMS)
Encapsulation Weak (all public types are visible) Strong (only exports packages are visible)
Dependencies Implicit (relies on order, "JAR Hell") Explicit (requires declarations)
Package Rules Split packages allowed (same package in multiple JARs) No split packages allowed (single module owns the package)
Resolution Flat search, first-come, first-served Graph-based, strict resolution
Runtime Issues Common (e.g., NoClassDefFoundError) Minimized by early detection
Default Module Unnamed Module Named Modules, Automatic Modules

 

3. Module Resolution and Module Graph

When a modular Java application starts, the Java Virtual Machine doesn't just blindly load JARs. Instead, it engages in a sophisticated process called module resolution. This involves:

  1. Scanning the Module Path: The JVM first examines all modules specified on the module path.
  2. Reading Module Descriptors: For each module found, it reads its module-info.class file to understand its name, its explicit requires dependencies, and its exports.
  3. Building the Module Graph: The JPMS then constructs a directed graph where each node is a module and directed edges represent requires relationships. This graph is built by iteratively adding all directly and transitively required modules until the set of observable modules is complete.
  4. Verification: During this construction, the JPMS performs rigorous checks:
    • Uniqueness: Ensures no two modules have the same name.
    • No Cycles: Detects circular dependencies, though cycles are generally allowed in requires relationships.
    • No Split Packages: Verifies that a package is not defined by more than one module in the graph.
    • Completeness: Confirms that all required modules are actually present on the module path.

If any of these checks fail, the application will not start, providing early detection of configuration issues that would have previously led to mysterious runtime errors. This robust resolution process is a cornerstone of JPMS's reliability.

4. The Base Module (java.base)

The java.base module is the bedrock of the Java Platform Module System. It's the fundamental module that every other module implicitly requires, even if you don't explicitly add requires java.base; in your module-info.java file (though it's good practice for clarity). This module contains the most basic and essential functionalities of the Java SE Platform, including:

  • Core Java Language APIs: Packages like java.lang (e.g., ObjectStringSystemThread), java.util (e.g., collections, dates), and java.io (e.g., input/output streams).
  • Fundamental Concurrent Utilities: Basic concurrency primitives.
  • Reflection APIs: Core reflection capabilities.
  • Other essential utilities: Foundational classes needed by virtually any Java application.

Its implicit requirement ensures that all modules access these core building blocks without redundant declarations, making it the unavoidable root of every module graph.

5. Runtime Images (jlink)

Prior to Java 9, deploying a Java application meant bundling your code with a full JDK or JRE installation, which could be quite large. The jlink tool, introduced with JPMS, changes this. It analyzes your application's module graph (including your application modules and the JDK modules it depends on). 

Then, it creates a custom runtime image that includes only the required JVM parts (like Java.baseJava.desktop, etc.). jlink can consume modules packaged as modular JARs or the JDK's own .jmod files.

Benefits of jlink:

  • Reduced Size: Significantly smaller deployment packages include only essential modules, which is particularly beneficial for cloud deployments, microservices, and containerized environments.
  • Improved Security: By excluding unused modules, you reduce the attack surface. If a vulnerability is found in a module your application doesn't use, it won't be in your custom runtime.
  • Faster Startup: A smaller runtime means fewer classes to load, potentially leading to faster application startup times.
  • Self-Contained Applications: The resulting runtime image is self-contained, meaning you don't need a pre-installed JDK/JRE on the target machine. This simplifies deployment.

How jlink works (High-Level):

You typically use the jlink command-line tool, specifying your application's modules and the desired output directory. It then analyzes your module graph and constructs the runtime.

# Example jlink command
jlink --module-path <path-to-your-modules>:<path-to-jdk-modules> \
      --add-modules <your-main-module>,<other-needed-modules> \
      --output my-modular-app-runtime

This effectively "links" your application with a minimal, optimized JVM, making deployment much more efficient and secure. This is a game-changer for many modern Java deployments.

Beyond the use of Java Modules for robust application architecture, the future of software development is deeply integrated with AI. Elevate your skills to build intelligent applications with the Microsoft Gen AI Mastery Certificate for Software DevelopmentLearn to leverage new-age Generative AI tools and build solutions for tomorrow.

Also Read: Types of Variables in Java: Java Variables Explained

Having explored the advanced technicalities of Java Modules, including packaging, runtime behavior, and image creation, it's natural to consider their real-world impact. 

The Dual Side of Modularity: Benefits and Challenges of Java Modules

This section provides a balanced perspective on Java Modules, first highlighting the compelling advantages driving their adoption in modern application development, then candidly addressing the common hurdles and considerations developers may face during implementation.

How Modules Elevate Java Development: The Benefits

Java Modules offer a significant leap forward in application design, moving beyond the limitations of the traditional classpath. They enforce robust boundaries, streamline dependencies, and enable optimized runtimes, fundamentally changing how we build scalable and maintainable Java software. Understanding these core advantages clarifies why embracing modularity is a strategic decision for modern development.

Benefit Description Impact
Strong Encapsulation Hides internals, only exporting public APIs. Allows teams to build independent components with clear API boundaries, reducing accidental misuse and fostering robust, secure applications.
Reliable Configuration Validates dependencies at build and runtime. Developers gain confidence their applications will start predictably, eliminating "JAR Hell" and leading to fewer debugging hours.
Scalable Platform Create custom, minimal JVM runtimes (jlink). Operations teams deploy significantly smaller application bundles, reducing disk space, speeding up transfers, and enabling quicker startup in containerized environments.
Improved Performance JPMS restricts class loading to declared dependencies only, speeding up application startup and reducing memory overhead Developers experience faster feedback loops; in production, this translates to more efficient resource utilization and a snappier user experience.
Easier Parallel Dev. Clear APIs enable independent teamwork. Development teams can work more independently, accelerating development cycles as changes in one module are less likely to inadvertently break another.

Challenges and Considerations Of the Java Platform Module System

While powerful, adopting Java Modules comes with its own set of challenges. This sub-section will address common hurdles and provide insights into navigating them effectively. Here's a concise table summarizing the challenges for Developers and Architects when adopting the Java Platform Module System (JPMS):

Category Challenge for Developers Challenge for Architects
Learning Curve Understanding new JPMS concepts (module-info.java, exports, requires, etc.) Designing truly modular architectures with clear module boundaries
Legacy Code Refactoring monolithic applications; resolving strong encapsulation and cyclic dependency issues Guiding the migration of existing codebases; ensuring architectural consistency during refactoring
Dependencies Integrating non-modular third-party libraries; managing --add-exports/--add-opens flags Evaluating ecosystem maturity for JPMS; establishing dependency governance within the module graph
Build & Tooling Adjusting build scripts (Maven/Gradle); ensuring IDE compatibility Defining deployment strategies (e.g., jlink custom runtimes); ensuring tooling supports architectural vision
Runtime Issues Dealing with reflection limitations, managing mixed classpath/module path scenarios Adapting architectural patterns to JPMS (e.g., cross-cutting concerns); balancing encapsulation with practical needs
Design & Ops Granularity decisions for modules, ensuring code conforms to explicit module contracts Ensuring prescriptive architecture matches descriptive, managing module evolution and versioning

Also Read: Python vs Java: Mastering the Best Language For Your Career

Expand Your Java Expertise: A Journey with upGrad

To expand your Java expertise and leverage modularity effectively, focus on hands-on practice with JPMS tooling like jlink and dive into real-world modular project structures. Continuously experimenting with module declarations and understanding error messages will solidify your grasp on this powerful paradigm.

Understanding modern Java development can be overwhelming. Without structured guidance, grasping advanced concepts like Java Modules and becoming a versatile full-stack engineer often feels like an uphill battle. upGrad bridges this gap, providing industry-aligned tech courses to equip you with in-demand skills.

 Here are some additional courses that can further enhance your development skills:

Still struggling to find the right Java software development course to match your goals in 2025? Reach out to upGrad for personalized counseling and valuable insights, or visit your nearest upGrad offline center for more details.

Boost your career with our popular Software Engineering courses, offering hands-on training and expert guidance to turn you into a skilled software developer.

Master in-demand Software Development skills like coding, system design, DevOps, and agile methodologies to excel in today’s competitive tech industry

Stay informed with our widely-read Software Development articles, covering everything from coding techniques to the latest advancements in software engineering.

Frequently Asked Questions (FAQs)

1. How does the module system specifically prevent NoClassDefFoundError at runtime, which was familiar with the classpath?

2. What happens if I have a package with the same name in two different JARs and try to use them on the module path?

3. Can a module expose some packages only to specific other modules, rather than to everyone?

4. What is the main benefit of jlink over just bundling a full JDK/JRE with my application?

5. How does the requires transitive keyword simplify module declarations for consuming modules?

6. What's the purpose of opens versus exports in a module-info.java file?

7. Can a named module read a package from the traditional classpath?

8. What is the significance of java.base being implicitly required by all other modules, and how does it relate to preventing NoClassDefFoundError for core classes?

9. How can the module system help prevent supply chain attacks or accidental inclusion of vulnerable code?

10. What are automatic modules, and why are they considered a temporary solution during migration?

11. How does JPMS enhance the security of an application at runtime?

Rohan Vats

408 articles published

Software Engineering Manager @ upGrad. Passionate about building large scale web apps with delightful experiences. In pursuit of transforming engineers into leaders.

Get Free Consultation

+91

By submitting, I accept the T&C and
Privacy Policy

India’s #1 Tech University

Executive PG Certification in AI-Powered Full Stack Development

77%

seats filled

View Program

Top Resources

Recommended Programs

upGrad

AWS | upGrad KnowledgeHut

AWS Certified Solutions Architect - Associate Training (SAA-C03)

69 Cloud Lab Simulations

Certification

32-Hr Training by Dustin Brimberry

upGrad

Microsoft | upGrad KnowledgeHut

Microsoft Azure Data Engineering Certification

Access Digital Learning Library

Certification

45 Hrs Live Expert-Led Training

upGrad

upGrad KnowledgeHut

Professional Certificate Program in UI/UX Design & Design Thinking

#1 Course for UI/UX Designers

Bootcamp

3 Months