Boost Your Coding Skills: The Ultimate Guide to Java Modules

Zied MELLITIZied MELLITI
4 min read

Java has been powering enterprise systems for decades, but until Java 9, it lacked a true modular system. Everything lived in the classpath, and every class could see every other class — even when it shouldn’t. This lack of encapsulation made systems harder to maintain, scale, and secure.

That changed with the Java Platform Module System (JPMS).

In this article, we’ll explore Java modules using a real-world e-commerce example, show how they improve architecture, and share tips to adopt them effectively in your codebase.


Why Java Modules?

Modules give you:

  • Strong encapsulation: Only explicitly exported packages are visible.

  • Reliable dependency management: No more "classpath hell."

  • Smaller, faster applications: Thanks to tools like jlink and jdeps.

  • Better security: Internal APIs are no longer accidentally accessible.

Think of modules as first-class citizens in your architecture — not just folders, but building blocks with contracts.


Real-World Example: E-Commerce Checkout System

To understand Java modules in a real context, let’s break down and build a modular E-Commerce Checkout System. This system follows clear separation of concerns by dividing responsibilities into distinct modules. This makes the application easier to maintain, test, and extend over time.

ecommerce-checkout/
├── checkout.service/        → Handles the checkout process and business flow
│   ├── module-info.java
│   └── com.store.checkout/
│       └── CheckoutManager.java
├── inventory.service/       → Manages product catalog and stock levels
│   ├── module-info.java
│   └── com.store.inventory/
│       ├── InventoryService.java
│       └── Product.java
├── common.utils/            → Provides shared utilities like logging and validation
│   ├── module-info.java
│   └── com.store.common/
│       ├── Logger.java
│       └── Validator.java

Module Responsibilities

ModuleResponsibilityExportsRequires
common.utilsShared helper utilities (e.g., logging, input validation)com.store.common
inventory.serviceInventory management (e.g., checking and updating stock)com.store.inventorycommon.utils
checkout.serviceCheckout flow (validation, stock check, payment logic)com.store.checkoutinventory.service, common.utils

Architecture Diagram :


Module 1: common.utils

module-info.java

javaCopierModifiermodule common.utils {
    exports com.store.common;
}

Logger.java

javaCopierModifierpackage com.store.common;

public class Logger {
    public static void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Validator.java

javaCopierModifierpackage com.store.common;

public class Validator {
    public static boolean isValidQuantity(int qty) {
        return qty > 0;
    }
}

Module 2: inventory.service

module-info.java

javaCopierModifiermodule inventory.service {
    requires common.utils;
    exports com.store.inventory;
}

Product.java

javaCopierModifierpackage com.store.inventory;

public class Product {
    private final String name;
    private final int stock;

    public Product(String name, int stock) {
        this.name = name;
        this.stock = stock;
    }

    public boolean isInStock(int quantity) {
        return stock >= quantity;
    }

    public String getName() {
        return name;
    }
}

InventoryService.java

javaCopierModifierpackage com.store.inventory;

import com.store.common.Logger;

public class InventoryService {
    public boolean checkAvailability(Product product, int qty) {
        Logger.log("Checking inventory for " + product.getName());
        return product.isInStock(qty);
    }
}

Module 3: checkout.service

module-info.java

javaCopierModifiermodule checkout.service {
    requires inventory.service;
    requires common.utils;
    exports com.store.checkout;
}

CheckoutManager.java

javaCopierModifierpackage com.store.checkout;

import com.store.inventory.InventoryService;
import com.store.inventory.Product;
import com.store.common.Validator;
import com.store.common.Logger;

public class CheckoutManager {
    private final InventoryService inventoryService = new InventoryService();

    public void checkout(Product product, int quantity) {
        if (!Validator.isValidQuantity(quantity)) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }

        if (inventoryService.checkAvailability(product, quantity)) {
            Logger.log("Checkout successful for: " + product.getName());
        } else {
            Logger.log("Checkout failed: Insufficient stock for " + product.getName());
        }
    }
}

Benefits of This Design

Encapsulation

Each module exposes only what’s necessary. No other module can access unexported internals.

Reusability

common.utils can be reused in other parts of the system like payments or shipping.

Maintainability

Teams can work on modules independently. Changes in inventory.service won't break checkout.service unless APIs change.

Performance

Using tools like jlink, you can package only the modules you need into a lightweight runtime image.


Best Practices for Working with Java Modules

  1. Don’t export everything: Use exports selectively.

  2. Avoid circular dependencies: Keep your module graph clean.

  3. Use opens only when required (e.g., for reflection in frameworks).

  4. Use provides/uses for dynamic service loading.

  5. Modularize incrementally: You can migrate existing codebase module-by-module.


Tooling That Helps

  • jdeps: Analyze class dependencies.

  • jlink: Create custom JDK runtimes with only required modules.

  • javac --module-path: Compile modular projects.

  • Maven/Gradle: Both support modular builds natively with a few tweaks.


When Should You Use Java Modules?

Use JPMS if you:

  • Are building libraries with a public API.

  • Want better separation in a large monolith or modular microservices.

  • Need secure or minimal JDK runtimes (e.g., IoT, Docker, or CLI tools).

  • Care about long-term maintainability, structure, and team autonomy.

You might skip JPMS if:

⚠️ You heavily rely on frameworks that use deep reflection (Spring Boot, Hibernate), unless you configure opens properly.
⚠️ You’re building small, quick POCs that don’t need modular structure.


Wrapping Up

Java Modules are more than a language feature — they’re an architectural tool. They help you structure large systems, enforce contracts, and build safer, more maintainable applications.

If you're writing Java in 2025 and beyond, learning JPMS is worth it. Even if you're not ready to modularize everything today, understanding modules can improve how you design and organize code.

0
Subscribe to my newsletter

Read articles from Zied MELLITI directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Zied MELLITI
Zied MELLITI

As a Senior Software Engineering Consultant, I believe in mentoring and coaching others to help them reach their full potential and achieve their professional goals. I am dedicated to inspiring a culture of excellence, continuous learning, and collaboration, ensuring the delivery of high-quality software solutions. Throughout my career, I have collaborated with diverse teams to deliver successful software projects utilizing Agile methodologies. I have also led innovation initiatives to improve processes, tools, and technologies, driving efficiency and productivity. My experience and expertise have allowed me to develop a strong foundation in software engineering, project management, and innovation management.