Skip to content

Modularity Patterns with JPMS: Acyclic Dependencies

Modularization is the process of organizing code into cohesive units. This process is sometimes tedious and hard to do consistently. Patterns can help to make modularization simpler by providing solutions for some common problems. [Kno12] and [Sand&Bak17] are two books which describe patterns that are concerned with modularity. In this blog post series, I am going to show you how they can be applied using the Java Platform Module System (JPMS).

Sample code for this post can be found on GitHub.

The Pattern: Acyclic Dependencies

Dependencies between modules should never form a cycle. Different kinds of cycles include direct dependencies as well as indirect dependencies over three or more modules. In the diagram below there are two cycles, one between module 1 and 2 and the other indirectly over module 3.


When refactoring existing code to modules, it is often the case that you discover cyclic dependencies between packages that you would like to separate in different modules. However, when you refactor towards acyclic dependencies usability and reuse of the code is improved as you will see in the example below.

Implementation

The simplest way to avoid cycles is to remove dependencies altogether. So before applying any of the methods to break cycles think through the involved dependencies. Are they absolutely required or is there a way to remove them?

If they are required, there are multiple ways to break a cycle in your module dependency graph. These are escalation, demotion, and call-back. The following sections show an example of each of these methods.

The sample

Imagine a web shop application that we want to modularize. It has three components: Product, Inventory, and Sale. When a product is displayed to the user during a session, the sales page checks the inventory for its availability. According to a business requirement, when there are multiple sales going on in parallel, we need to ensure that the availability is always guaranteed. Therefore, the inventory needs to take the baskets of these sales into account when calculating the availability. Baskets are located in the sale component. These dependencies form a cycle between the sale and inventory components as shown below.

The Refactoring

When we refactor the components to modules without change we end up with the following module descriptors:

module opus.inventory {
  exports com.opus.inventory;

  requires opus.sale; <--- cycle
  requires opus.product;
}

module opus.sale {
  exports com.opus.sale;

  requires opus.inventory; <---- cycle
  requires opus.product;
}

module opus.product {
  exports com.opus.product;
}

This code fails to compile because of the cycle. We need to break it by using one of the following methods.

Escalation

The first method is to use escalation. We design an additional module called sale process. This module has a dependency on both the inventory and sale module. When the system shows a product to the user, the sale process is responsible for calculating the availability. It asks the inventory for the stock count and checks the baskets from the sale module. With this change, we can remove the dependencies between sale and inventory altogether.

Demotion

Another way to break the cycle is to create a new module called basket. Both the sale and the inventory module depend on the basket module. We ask the inventory for the availability like before. The inventory, in turn, checks the baskets for ongoing sales.

module opus.basket {
  exports com.opus.basket;
}

module opus.inventory {
  exports com.opus.inventory;

  requires opus.basket;
}

module opus.sale {
  requires opus.inventory;
  requires opus.basket;
}

This is called demotion. We demote functionality to a module on a lower level.

Callback

The third method to break cycles is to use a callback. To implement it we need a new JPMS service called OngoingSalesCalculator with a method ongoingSalesCountFor(Product product). This interface is located inside the inventory module (or a separate module) and implemented within the sale module. The inventory uses the callback to calculate the availability now instead of checking the baskets directly.

module opus.inventory {
  exports com.opus.inventory;

  uses com.opus.inventory.OngoingSalesCalculator;
}

module opus.sale {
  requires opus.inventory;

  provides com.opus.inventory.OngoingSalesCalculator 
    with com.opus.sale.impl.OngoingSalesCalculatorImpl;
}

With this callback in place, we can remove the dependency from the inventory module on the sales module. By using a JPMS service, we don’t need a main class or a framework to do the wiring. We obtain the callback service directly from the service loader inside the inventory module.

When looking at the above descriptors, it may seem that there is still a cycle between those modules. There is actually one but only at runtime. The inventory module does not depend on the sales module during development and compile time but uses the calculator service only at runtime.

Wrap Up / Final Thoughts

When an existing code base is refactored into modules, the resulting dependency graph may contain cycles. These not only prevent code reuse but make it harder to think about the code and change it. There are multiple ways to break cycles, three of which we have seen in this post. When we break the cycles, individual modules can be reused and should be simpler to maintain.

The next part of this series about modularity patterns will take a look at abstractions in the context of modules. We will discover three patterns which help us design resilient modules: Abstract Module, Separate Abstraction and API Modules.

[Kno12] K. Knoernschield: Java Application Architecture – Modularity Patterns with Examples Using OSGi (homepage)
[Sand&Bak17] Sander Mak & Paul Bakker: Java 9 Modularity – Patterns and practices for developing maintainable applications (homepage)

An den Anfang scrollen