What Is Modularity in Java?

A feature introduced since Java 9

Dieter Jordens
Feb 21 · 7 min read
Image for post
Image for post
Photo by Simon Goetz on Unsplash

In this piece, I want to tell you everything you need to know about Java modules. I intend to help Java developers who could get asked about this as an interview question or those who are looking to get Java 11 OCP certified. If you have your reasons for reading about modules, that’s fine too! Feel free to leave a comment if that’s the case, because I’d love to hear from you.

There will be more than just theory (and I’ve left out the more advanced stuff like jdeps), so don’t be afraid to be overwhelmed!

What is a Module?

A module is a Java program component introduced in Java 9. Pre Java 9, classes resided in packages and classes within those packages were found by class loaders. From Java 9 onwards modules act in-between packages and class loaders. The resources and packages (and classes within those packages) can be encapsulated from other modules in levels that were just not possible before. It brings defensive development to a higher level in Java. As a developer, that’s just awesome to hear!

From now on it will only be possible to use explicitly exported public classes from other modules. Developers that work without modules will use unnamed modules from now on. These unnamed modules will export all their packages. This is great for backwards compatibility, however, I’d strongly suggest using named modules as early as possible. The migration process can be time-consuming, especially for developers that have little to no knowledge of the module system.

A module is a uniquely named, reusable group of related packages, resources and a module descriptor that specifies the following:

  • name
  • dependencies
  • exported packages
  • provided packages
  • used services
  • which packages are open

Name

A module declaration specifies a new named module. I will give some examples of valid module declarations:

module example {}

The example module is a perfectly valid module name, but a Java module must be given a unique name. It’s recommended to give a Java module the same as the name of the root Java package contained in the module, if possible.

A module name consists of one or more Java identifiers separated by “.” tokens. So this gives us:

module continuum.be.example {}

A Java module follows the same naming rules as Java packages. I would advise not to use underscores since the underscore character might turn into a reserved identifier in the future. Apart from that, I’d also suggest keeping every part of the module name lower case, for the sake of consistency.

Dependencies

On the one hand, a named module specifies dependencies on other modules to define the universe of classes and interfaces available to its code. A dependency is what is expressed by a requires directive. In the following module declaration, the continuum.be.example module has a direct dependency on the continuum.be.accounting module.

module continuum.be.example {requires continuum.be.accounting;}

There is also a requires static directive to indicate that a module is required at compile-time, but is optional at runtime. This is known as an optional dependency. Finally, you can also use the transitive keyword to ensure that other modules reading your module also read that dependency, known as implied readability.

Exported Packages

A Java module must export all packages in the module that are accessible for other modules using the module. A package can only be exported by a single Java module, otherwise, this will lead to a ResolutionException at runtime because a so-called split-package is created.

The exported packages are declared in the module descriptor. Here’s one for a simple export declaration:

module continuum.be.accounting {exports continuum.be.accounting;}

No sub-packages of the exported package are exported. That means, that if the accounting package contained a subpackage named util then the com.continuum.accounting.util package is not exported just because com.continuum.accounting is. You also don’t have to export the parent package, if you don’t want to.

Provided Packages

A Java module that wants to implement a service interface from another module must take the following steps:

  • Require the other module, in this case com.continuum.service.
  • Implement the service interface OurService with a Java class OurServiceImpl.
  • Declare the service interface implementation using the provides <interface> with <implementation class> notation.
module com.continuum.implementation {requires com.continuum.service;provides com.continuum.service.OurServicewith com.continuum.service.OurServiceImpl}

Used Services

Once you have a service interface module (e.g. JPA) and a service implementation module (e.g. Hibernate), you can create a client module that uses the service. To use the service in a modular way, you have to require the interface module, e.g. com.continuum.service and declare that you will use it using the uses keyword. An example of this could be the following:

module com.continuum.client {requires com.continuum.service;uses com.continuum.service.OurService;}

The advantage of not having to declare the implementation module is that the implementation module can be exchanged without breaking the client’s code. You can decide what service implementation to use when you assemble the application (e.g. by specifying the version in the pom.xml file when you’re using maven). This means that the client and the service interface module are decoupled from the service implementation module.

Now the service client module can look up the service interface implementation at runtime, like this:

ServiceLoader<GenerationService> load = ServiceLoader.load(GenerationService.class);

Which Packages Are Open

The opens keyword specifies the name of a package to be opened by the current module. For code in other modules, this grants access at run time (but not compile-time) to the public and protected types in the package, and the public and protected members of those types. It also grants reflective access to all types in the package, and all their members, for code in other modules.

It’s also possible to open it up for only one or more modules using the opens … to keywords. It’s strongly advised not to use the open keyword to open up an entire module, but who am I to judge what you do in your spare time — whatever floats your boat!

How About Putting This Theory Into Practice?

I’ve experimented with the Service Loader API, maven, and modules, and created a small fitness application. This shows how easy it can be to create a modular application and it could be a good starting point to create more complex industrial applications.

At first, starting with a Spring JPA application, it can be challenging to understand what’s going on in the new module system. You could end up with an application not even compiling.

This fitness application highlights the Service Loader API which is a less known alternative to dependency injection. For small applications introducing dependency injection from a well-known framework can increase the size of the application enormously, which can be bad for programs that have to worry about the memory being used. Yes, we can do it the native way!

Image for post
Image for post
Photo by Clément H on Unsplash

Exercise Module

The first module of our application is the exercise module, our service provider interface module. The exercise module exports one package, com.continuum.generation.spi, and is entirely uncoupled from implementations. If this package changes, it has to become compliant with the interface and not the other way around.

In this package, we have a GenerationService interface, that allows us to generate a Schedule (e.g. running schedule, fitness schedule) for a specific fitness Level which is just a regular enum.

GenerationService.java
Schedule.java

Sports Module

The second module of our fitness application is the sports module. This module provides two implementations for the exercise spi. Since they concern two different domains in sports I’ve put the running and fitness schedule generators in a separate package.

The implementation classes are abstracted away by the two factories (FitnessFactory and RunningFactory). This could allow different schedules to be generated depending on different parameters (e.g. based on current fitness level).

It shows that you don’t have to put your implementation classes inside your module declaration. However, it’s certainly possible to do so.

module-info.java
FitnessFactory
FitnessGenerationServiceImpl
FitnessSchedule

Main Module

Finally, the main module will be our client-like module. This requires the exercise SPI and has no direct dependencies on the implementation. We also indicate that we are going to use the GenerationService. The implementations provided will depend on the ones that are imported at run time, which is something we specify in the maven file.

We do need to specify in our pom file both the dependency that provides the interface we’re going to use, which is the exercise module, and the dependency that provides the implementation(s), which is the sports module in this case.

module-info.java
pom.xml

If we now run the following class in the main module:

Main.java

We obtain the following two lines of output:

  • Fitness schedule from a Continuum Craft Lead with level: Advanced
  • Running schedule from a Continuum Craftsman with level: Intermediate

Not bad! The user of the provided modules can work with the code he now has available at run time or provide a GenerationService implementation himself. This makes it easy to provide some mock implementation as well.

What To Do Now?

Isn’t that neat?

There are more sports than just running and fitness — why don’t you add a beer pong schedule?

You can find the repo with more details right here: https://github.com/djFooFoo/fitness. Feel free to experiment further with how modules work in Java.

If something is missing or something that could be better explained, leave a comment and I will get back to you. Thank you for reading!

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store