Creating a Java Module with JPMS in NetBeans

Java's Platform Module System, introduced with JDK 9 and refined in every release since, gives developers a way to enforce strong encapsulation at the package boundary rather than relying solely on access modifiers. A well-designed module declares exactly what it needs from the outside world and exposes only the packages that form its public contract. This discipline pays off as projects grow beyond a handful of packages, particularly in enterprise codebases maintained by distributed teams.

NetBeans IDE has supported module-aware development since the early days of Project Jigsaw, and its project wizard, code completion, and refactoring tools understand module descriptors natively. For developers in Brisbane, Sydney, or Perth working on long-lived systems, that native support removes much of the friction that once made modular projects feel like a chore. The walkthrough below uses a stock Java SE setup available to anyone running a current JDK, so it works just as well on a developer laptop in Melbourne as on a build agent hosted in a Canberra data centre.

Preparing the Project and Module Layout

Open NetBeans and create a new Java Application, then right-click the project node and choose Properties. On the Sources tab, set the source level to a recent JDK that supports modules, such as 17 or 21, and confirm that the Project Format matches the latest Ant-based layout. Under the Libraries tab, leave the classpath empty for the module you are about to write and add your inter-module dependencies through the modulepath instead.

Before writing any code, it helps to know which module flavours you might encounter during a migration. The table below summarises the three categories the compiler recognises.

Module type Identifier Source of truth Reflection access
Named module Declared module-info.java Explicit requires and exports Allowed only for exported packages
Automatic module Derived from MANIFEST.MF or filename Implicit requires on every other module on modulepath Permissive, similar to code running on older runtimes
Unnamed module Classpath code, no descriptor The whole classpath Unrestricted by the JPMS rules

Once the project is configured for module-aware compilation, NetBeans will refuse to put a classpath entry on the modulepath or vice versa, which catches a common mistake before it becomes a runtime surprise. The NetBeans Blog homepage walks through the latest IDE builds and platform bundles if you want a refresher on which distributions to install.

Writing the Module Descriptor

Every named module needs a single source file named module-info.java, placed at the root of its source tree. Its name should follow the reverse-domain convention used by package names, so a module owned by an Australian fintech might be called au.com.example.payments rather than something generic. The body of the descriptor is a small DSL made of directives such as requires, exports, opens, uses, and provides.

A minimal descriptor for a small library looks like this:

module au.com.example.payments {
    requires java.base;
    requires java.sql;
    exports au.com.example.payments.api;
}

The most useful directives to know when starting out are gathered below.

  • requires declares a dependency on another named module at compile time and runtime.
  • requires transitive re-exports a dependency so downstream modules also see it.
  • exports makes a package available to every module that requires this one.
  • opens allows reflective access, which frameworks in Sydney and Melbourne banking stacks often rely on.
  • provides … with registers a service implementation consumed through ServiceLoader.

Requiring and Exporting Packages

Encapsulation is the whole point of JPMS, so being deliberate about what a module exposes pays dividends later. A common pattern in Australian government projects is to split a single large JAR into an internal API module, a public API module, and a service implementation module. The internal API module uses requires static to pull in a test-only library that is discarded at runtime, while the public API module keeps its dependency graph small so other teams can adopt it without dragging in heavyweight libraries.

When two modules need to share implementation details, prefer a small internal package that is exported to a named list of friends rather than opened globally. The NetBeans code analyser flags over-broad opens to the entire universe, which is a useful safety net for junior developers who may not yet appreciate the difference.

Service Loaders with Uses and Provides

JPMS formalised a decoupling mechanism that had been quietly available through ServiceLoader for years. A consumer module declares uses au.com.example.payments.spi.PaymentGateway, and any provider module on the modulepath can declare provides au.com.example.payments.spi.PaymentGateway with au.com.example.bank.NABGateway;. At runtime the JDK returns every implementation it discovers, ordered by the position of modules on the modulepath.

This pattern is handy when migrating a monolithic monolith that serves customers across AEST and AEDT time zones. A team in Adelaide can ship a new gateway implementation as its own module without recompiling the consumer, and a rollout pipeline can swap modules between environments simply by editing the launch command.

Building and Running a Modular Application

Running a modular application from inside NetBeans is straightforward once the modulepath is configured. Right-click the project, choose Run, and the IDE assembles the module graph and launches the main class through java. To reproduce the same behaviour from a terminal on a build server, use a command such as java --module-path out --module au.com.example.payments/au.com.example.payments.Main.

For distribution, jlink produces a custom runtime image that bundles only the modules your application actually uses, trimming a typical JDK from around 200 MB to a fraction of that size. A smaller image is cheaper to ship to remote branches in Hobart or Darwin that rely on slower satellite links, and it also reduces the attack surface of the deployment.

Common Pitfalls Worth Knowing

Split packages are the first trap newcomers meet: the same fully qualified class appearing in two different JARs causes the linker to refuse the application. Automatic modules are a convenience, but they expose every internal package, which can quietly widen a previously tight contract.

A short checklist of issues to look for during code review:

  • Split packages: two JARs contributing the same fully qualified class to the modulepath.
  • Automatic modules: declared without a module-info.java, so they leak every package.
  • Over-broad opens: making reflective access available to the whole world when only one framework needs it.
  • Missing requires static: pulling test-only libraries into the production runtime image.

The NetBeans Blog publishes a deeper catalogue of post categories covering EJBs, session beans, and message-driven beans if you want to widen the conversation beyond JPMS itself.