Building A Microservice-Ready Monolith

By | February 2, 2023
Reading Time: 20 minutes

When developing programs that are larger than minuscule I prefer to structure them in a fashion as once inspired by an article by Simon Brown named “An Architecturally Evident Coding Style” that I read some years ago. Not only does this help me to organize my code in a, to me, familiar way but it also make it easier to for multiple developers to work on the same code at the same time. In addition, if, at some point, one or more components of the program need to be broken out to standalone software units, this will be significantly easier.

Monolith picture from https://blazblue.wiki/index.php?curid=7254. License CC BY-NC-SA 2.0.
Monolith picture from https://blazblue.wiki/index.php?curid=7254. License CC BY-NC-SA 2.0.

Given that I quite often think of developing software in teams and software maintenance, I will examine two ways to verify/enforce the proposed architecture; ArchUnit and the Java Platform Module System, the latter being the result of Project Jigsaw. In addition I will also take a look at Spring Modulith, which is an adaption of ArchUnit for Spring Boot applications.

This article will describe the proposed architecture and then go on to have a look at whether ArchUnit may be useful when enforcing the architecture.

Disclaimer: I am not a user-interface developer and my focus is usually, if not always, on what in a layered architecture would be described as the stuff behind the presentation layer.

Update 2024-03-31: Originally intended to be two articles, I decided to skip writing about enforcing architectural constraints using annotations in the code since I do not believe in the concept.

Example Application

The example application I have written is a simplistic shopping application which is far from complete – I have implemented only one single use cases; Add Item To Cart. The sequence diagrams for the use case looks like this:

Add Item To Cart sequence diagram for the example application.
Add Item To Cart sequence diagram for the example application.

The example application has been implemented using Spring Boot 3 and Java 19 and it is available on GitHub.

Organizing the Code

For the sake of completeness, I will briefly describe how I organize the code. I have shamelessly stolen from Simon Brown but any quirks and errors are mine.

Instead of separating an application in horizontal layers, as in the n-tier or multilayer architecture, the application is separated in vertical slices. If desired, horizontal layering may still be applied inside these slices.

Outline of the organization of code inside a module.
Outline of the organization of code inside a module.

In the above diagram there are two modules; the shoppingcart and warehouse modules. Examining the shoppingcart module, it is structured as shown with the api and the configuration parts being the only parts that are accessible by other components. The API defines an abstract type describing the operations and the data types of the input and output messages. In the Java programming language, this would be an interface. In addition, the module is divided into four layers, if the part containing the api and the configuration is to be considered as a layer of its own separated from the implementation.
Apart from exposing an API, a component may have to expose something that allows it to be instantiated and started. In Java programs using the Spring framework, I have used public @Configuration classes with non-public @Bean methods allowing for the inclusion of such classes in an application configuration or tests without exposing information about the beans in the configuration. Such configuration classes are to be located in the configuration part in the above figure.

Note that I have chosen to make the domain classes, located in the domain part, visible only to the module in which they reside. This makes each module a bounded context, as defined in Domain-Driven Design. If a module needs to pass information about a domain entity to another module, this information should be conveyed in the form of, for example, a JSON representation. There may be a need to add an anti-corruption layer to a module, or between modules, if the information passed to other modules have to be adapted in some way but such considerations are outside the scope of this article.

Access Rules

With thoughts on how to organize code and an example program to act as a guinea pig, the time has now come to turn the attention to how to make sure that the program is properly organized. This figure shows how different parts of the example application are to be disallowed to access other parts of said application.

Diagram showing access rules in the context of the example application.
Red arrows shows access/dependencies that are not allowed.
Diagram showing access rules in the context of the example application.
Red arrows shows access/dependencies that are not allowed.

In addition to the above figure, I want to put the access rules into words but first some definitions:

TermDefinition
ModuleCode and related artifacts that is a candidate to be broken out into a micro-service.
PackageThe package concept as defined by the Java programming language. Used to group related code but also for code access control.
ArtifactCode or related artifacts used in an application.
Green artifactArtifact belonging to the api or configuration packages of a module like, for example, the warehouse module.
Red artifactArtifact belonging to a red package, for example warehouse, and any of its subpackages except for the api and configuration packages.
Purple artifactApplication artifact belonging to a package outside any module.

Access rules:

  1. Red artifacts in one module are not allowed to access red artifacts in other modules.
    Example: Code in any package in the warehouse package and its subpackages, with the api and configuration packages excluded, is not allowed to access code in the shoppingcart package or any of its subpackages, again with the api and configuration packages excluded.
  2. Purple artifacts are not allowed to access red artifacts.
    Example: Code outside of the modules is not allowed to access code in the warehouse package and its subpackages, with the api and configuration packages excluded.
  3. Red and green artifacts are not allowed to access purple artifacts.
    Example: Code in the warehouse package and its subpackages should not have dependencies to application code outside of modules.

The above rules will come in handy when writing tests that verify adherence to the desired architecture.

ArchUnit

ArchUnit is a library that can be used in JUnit or TestNG tests to verify architectural constraints of Java code.

This article will step-by-step explore ArchUnit with the purpose of arriving at tests that enforce the suggested way of organizing the code.

Test Class

Create a test class named ArchUnitTests in the test/java directory of the project with the following contents:

package se.ivankrizsan.monolithmicroservices;

import lombok.extern.slf4j.Slf4j;

/**
 * Tests the application's adherence to the proposed structure which purpose
 * is to allow for easier refactoring into microservices.
 *
 * @author Ivan Krizsan
 */
@Slf4j
public class ArchUnitTests {
    
}

While it cannot really be discerned at this stage, the above class is a JUnit 5 test class. As one may suspect, this class will contain the ArchUnit tests.

Listing Dependencies

As a first step of getting acquainted with ArchUnit, I wrote a method in the test class created earlier that will list the dependencies of each and every class in the example application. The listing below contains the entire test class in order to show imports.

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.util.Set;
import java.util.function.Function;

/**
 * Tests the application's adherence to the proposed structure which purpose
 * is to allow for easier refactoring into microservices.
 *
 * @author Ivan Krizsan
 */
@Slf4j
public class ArchUnitTests {
    public static final String APPLICATION_ROOT_PACKAGE = "se.ivankrizsan";

    /**
     * Lists the dependencies of the classes in this example application contained
     * in the specified root package.
     */
    @Test
    public void listClassDependenciesTest() {
        final JavaClasses theClassesToCheck = new ClassFileImporter()
            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
            .importPackages(APPLICATION_ROOT_PACKAGE);

        for (JavaClass theJavaClass : theClassesToCheck) {
            final Set<Dependency> theClassDependencies =  theJavaClass.getDirectDependenciesFromSelf();
            log.info("Class {} has the following dependencies:", theJavaClass.getName());
            theClassDependencies
                .stream()
                .map((Function<Dependency, Object>) inClassDependency -> inClassDependency.getTargetClass().getName())
                .sorted()
                .distinct()
                .forEach(inClassDependency -> log.info("    {}", inClassDependency));
        }
    }
}

Note that:

  • A ClassFileImporter is used to obtain information on the classes to list.
    The classes are located in the application root package and test test classes are filtered out.
  • The first loop iterates over the classes and the second loop iterates over the dependencies the current class from the first loop have to other classes.

Running the test, the following output can be observed in the console log:

20:17:48 Class se.ivankrizsan.monolithmicroservices.modules.warehouse.api.WarehouseService has the following dependencies:
20:17:48     java.lang.Double
20:17:48     java.lang.Long
20:17:48     java.lang.String
20:17:48     java.util.Optional
20:17:48     se.ivankrizsan.monolithmicroservices.modules.warehouse.exceptions.ProductNotInWarehouseException
20:17:48 Class se.ivankrizsan.monolithmicroservices.modules.warehouse.domain.ProductReservation has the following dependencies:
20:17:48     jakarta.persistence.Column
20:17:48     jakarta.persistence.Entity
20:17:48     jakarta.persistence.GeneratedValue
20:17:48     jakarta.persistence.GenerationType
20:17:48     jakarta.persistence.Id
20:17:48     java.lang.Double
20:17:48     java.lang.Long
20:17:48     java.lang.Object
20:17:48     java.lang.String
20:17:48     java.util.Objects

There is quite some output from the listClassDependenciesTest, most of which has been omitted in the above listing. We can see that all the classes of the example program are listed in “Class xxx has the following dependencies:” statements followed by the dependencies of the class in question.

Listing Slices

As of writing this article, ArchUnit provides two predefined checks of common architectural styles; one for the traditional layered architecture and one for the onion architecture. Neither of these two are on their own suitable for the architecture proposed in this article. After some searching, I came upon the ArchUnit concept of slices. The first attempt towards being able to verify the proposed code organization is to group the non-public module classes of the application in ArchUnit slices in order to be able to enforce the first access rule which says that “red artifacts in one module are not allowed to access red artifacts in other modules”. The approach used is to create slices of the code where each slice represent a module excluding the parts that other modules and parts of the program are allowed to access, namely the API and configuration of the module. The illustration below visualizes how the example program should be sliced.

Slicing the example application in non-public module code slices.
Slicing the example application in non-public module code slices.

The shoppingcart slice is green and the warehouse slice is blue. Artifacts, classes interfaces etc, that are not coloured do not belong to any slice.

Modules Slice Assignment

In order to slice the example application in slices containing non-public classes of modules, I created a subclass of the ArchUnit SliceAssignment class in the test-part of the application:

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;

import java.util.Optional;

/**
 * A {@code SliceAssignment} which maps non-public classes in modules to ArchUnit slices.
 *
 * @author Ivan Krizsan
 */
public class ModulesSliceAssignment implements SliceAssignment {
    @Override
    public String getDescription() {
        return "non-public parts of modules";
    }

    @Override
    public SliceIdentifier getIdentifierOf(final JavaClass inJavaClass) {
        /* Does the supplied class belong to a module? */
        final Optional<String> theModuleNameOptional = ArchUnitModuleUtils.moduleFromJavaClass(inJavaClass);
        if (theModuleNameOptional.isPresent()) {
            /*
             * Does the supplied class belong to a package in the module to which access
             * from anywhere is allowed?
             */
            if (ArchUnitModuleUtils.isLocatedInModulePublic(inJavaClass)) {
                /*
                 * Supplied class is located in a package in a module to which access from anywhere
                 * is allowed and public parts of modules are not to be included in slices so this class
                 * should not belong to any slice.
                 */
                return SliceIdentifier.ignore();
            } else {
                /*
                 * Supplied package is located in a module and access to the class should not be
                 * allowed from other modules.
                 */
                final String theModuleName = theModuleNameOptional.get();
                return SliceIdentifier.of(theModuleName);
            }
        }

        /* Ignore all classes that does not belong to the application. */
        return SliceIdentifier.ignore();
    }
}

Note that:

  • The method getIdentifierOf does the work of determining whether the supplied class belongs to a slice and, if so, which slice.
    Slices will have the same name as the modules in the application.

ArchUnit Module Utilities

The above slice assignment class uses a helper class which looks like this:

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.core.domain.JavaClass;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class containing utility methods related to modules.
 *
 * @author Ivan Krizsan
 */
public final class ArchUnitModuleUtils {
    /* Constant(s): */
    /** Package in which module packages are located. */
    public static final String MODULES_ROOT_PACKAGE_NAME = "modules";
    /** Regular expression used to find names of modules given a package name. */
    public static final String MODULE_NAME_REGEXP =
        "\\." + MODULES_ROOT_PACKAGE_NAME + "\\.([a-z0-9_]*)";
    /** Regular expression used to find names of first-level subpackages in modules given a package name. */
    public static final String MODULE_NAME_WITH_SUBPACKAGES_REGEXP =
        MODULE_NAME_REGEXP + "\\.([a-z0-9_]*)";
    public static final Pattern MODULE_NAME_REGEXP_PATTERN = Pattern.compile(MODULE_NAME_REGEXP,
        Pattern.CASE_INSENSITIVE);
    public static final Pattern MODULE_NAME_WITH_SUBPACKAGES_REGEXP_PATTERN =
        Pattern.compile(MODULE_NAME_WITH_SUBPACKAGES_REGEXP, Pattern.CASE_INSENSITIVE);
    /** Group in which module name can be found when there is a regexp match for a package name. */
    public static final int MODULE_NAME_REGEXP_GROUP_INDEX = 1;
    /**
     * Group in which module first-level subpackage name can be found when there is a regexp
     * match for a package name.
     */
    public static final int MODULE_SUBPACKAGE_REGEXP_GROUP_INDEX = 2;
    /** Name of first-level subpackages in modules that may be accessed from anywhere. */
    public static final List<String> MODULE_PUBLIC_PACKAGES = Arrays.asList("api", "configuration");

    /**
     * Determines whether the supplied Java class belongs to a module and, if so,
     * finds the name of the module the class belongs to.
     *
     * @param inJavaClass Java class which to determine whether it belongs to a module.
     * @return Optional containing name of module or empty if the class does not belong to a module.
     */
    public static Optional<String> moduleFromJavaClass(final JavaClass inJavaClass) {
        final String theJavaClassPackageName = inJavaClass.getPackageName();
        final Matcher theModuleNameMatcher = MODULE_NAME_REGEXP_PATTERN.matcher(theJavaClassPackageName);
        final boolean theModuleNameFoundFlag = theModuleNameMatcher.find();
        if (theModuleNameFoundFlag) {
            final String theModuleName = theModuleNameMatcher.group(MODULE_NAME_REGEXP_GROUP_INDEX);
            return Optional.of(theModuleName);
        }

        return Optional.empty();
    }

    /**
     * Determines whether the supplied Java class belongs to a module and, if so, finds the name
     * of the package in the module immediately below the module package that the class belongs to.
     * Example: The class se.ivankrizsan.modules.modone.api.ServiceInterface belongs to the module "modone"
     * and the module subpackage is "api".
     *
     * @param inJavaClass Java class which to determine whether it belongs to a module subpackage.
     * @return Optional containing name of module subpackage or empty if the class is not located
     * in a module subpackage.
     */
    public static Optional<String> moduleSubpackageFromJavaClass(final JavaClass inJavaClass) {
        final String theJavaClassPackageName = inJavaClass.getPackageName();
        final Matcher theModuleSubpackageMatcher = MODULE_NAME_WITH_SUBPACKAGES_REGEXP_PATTERN
            .matcher(theJavaClassPackageName);
        final boolean theModuleSubpackageNameFoundFlag = theModuleSubpackageMatcher.find();
        if (theModuleSubpackageNameFoundFlag) {
            final String theModuleSubpackageName = theModuleSubpackageMatcher
                .group(MODULE_SUBPACKAGE_REGEXP_GROUP_INDEX).toLowerCase();
            return Optional.of(theModuleSubpackageName);
        }

        return Optional.empty();
    }

    /**
     * Determines whether supplied class is located in a public package of a module.
     *
     * @param inJavaClass Java class which to determine whether it belongs to a module public package.
     * @return True if class belongs to a module public package, false otherwise.
     */
    public static boolean isLocatedInModulePublic(final JavaClass inJavaClass) {
        boolean theInModulePublicFlag = false;

        final Optional<String> theModuleSubpackageOptional = moduleSubpackageFromJavaClass(inJavaClass);
        if (theModuleSubpackageOptional.isPresent()) {
            final String theModuleSubpackage = theModuleSubpackageOptional.get();
            theInModulePublicFlag = MODULE_PUBLIC_PACKAGES.contains(theModuleSubpackage);
        }

        return theInModulePublicFlag;
    }
}

Note that:

  • A constant MODULES_ROOT_PACKAGE_NAME defines the name of the package in which the modules of the application reside.
  • The constant MODULE_PUBLIC_PACKAGES defines a list containing the names of packages at the first level in a module that are publicly accessible.
  • The method moduleFromJavaClass attempts to find the name of the module in which a Java class is located.
    Example: If a class is located in the package se.ivankrizsan.monolithmicroservices.modules.shoppingcart.api then it is located in the module with the name “shoppingcart”.
  • The method moduleSubpackageFromJavaClass attempts to find the name of the first-level package inside a module in which a Java class is located.
    Example: If a class is located in the package se.ivankrizsan.monolithmicroservices.modules.shoppingcart.api then it is located in a module subpackage with the name “api”.
  • The method isLocatedInModulePublic attempts to determine whether a Java class is located in a public part of a module, i.e. a part of a module which may be accessed from outside of the module and from other modules.
    In this example project, the “api” and “configuration” module subpackages and their contents are considered to be publicly accessible parts of a module.

List Slices Test

With the above classes in place, a new method can now be added to the test class. The following listing shows the test class with all other test methods omitted:

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.PackageMatcher;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.library.Architectures;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * Tests the application's adherence to the proposed structure which purpose
 * is to allow for easier refactoring into microservices.
 *
 * @author Ivan Krizsan
 */
@Slf4j
public class ArchUnitTests {
    public static final String APPLICATION_ROOT_PACKAGE = "se.ivankrizsan";


    /**
     * Lists the classes of the application, including test-classes, and the slice to which
     * each class belongs,if any, to having applied the {@code ModulesSliceAssignment} to group the
     * application classes into slices.
     */
    @Test
    public void listClassSliceTest() {
        final JavaClasses theClassesToCheck = new ClassFileImporter()
            .importPackages(APPLICATION_ROOT_PACKAGE);

        /* Maps Java classes to slices specifying which classes belong to a slice. */
        final SliceAssignment theClassToModulesSliceMapper = new ModulesSliceAssignment();

        /* List the slices and the classes belonging to each slice. */
        for (JavaClass theJavaClass : theClassesToCheck) {
            final SliceIdentifier theJavaClassSlice = theClassToModulesSliceMapper.getIdentifierOf(theJavaClass);
            final String theJavaClassName = String.format("%-120s", theJavaClass.getName());
            log.info("{} - {}", theJavaClassName, theJavaClassSlice.toString());
        }
    }
}

The new method uses the slice assignment developed earlier to obtain a slice identifier for each class in the example application.

Running the new method, the following list of slices and classes are logged to the console:

20:52:37 se.ivankrizsan.monolithmicroservices.MonolithMicroservicesApplicationTests                                               - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.api.WarehouseService                                              - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.domain.ProductReservation                                         - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.implementation.WarehouseServiceImplementation                     - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest           - SliceIdentifier[shoppingcart]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.exceptions.WarehouseException                                     - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductReservationRepository                          - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.persistence.ShoppingCartItemRepository                         - SliceIdentifier[shoppingcart]
20:52:37 se.ivankrizsan.monolithmicroservices.ArchUnitTests                                                                       - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.MonolithMicroservicesApplication                                                    - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.implementation.WarehouseServiceImplementationTest                 - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementation               - SliceIdentifier[shoppingcart]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.api.ShoppingCartService                                        - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.configuration.ShoppingCartConfiguration                        - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementation$1             - SliceIdentifier[shoppingcart]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.exceptions.ProductNotInWarehouseException                         - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductRepository                                     - SliceIdentifier[warehouse]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.shoppingcart.domain.ShoppingCartItem                                        - SliceIdentifier[shoppingcart]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.configuration.WarehouseConfiguration                              - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.ModulesSliceAssignment                                                              - SliceIdentifier[]
20:52:37 se.ivankrizsan.monolithmicroservices.modules.warehouse.domain.Product                                                    - SliceIdentifier[warehouse]

We can see that classes of modules, except those in the api and configuration packages, all belong to a slice. Note that test classes, such as the ShoppingCartServiceImplementationTest, also belong to slices. This is positive since if extracting a module to a standalone service, one will also want to extract the corresponding tests and thus these should abide by the same rules as the rest of the module.

Access Rule One

Access rule number one listed earlier states that there are to be no dependencies between non-public parts of modules. Being able to slice the application code in slices where each slice contains the non-public part of a module, the next step is to write a test that will fail if there are any dependencies between non-public parts of two or more modules.

Access Rule One Test

This time the new test method will be a real test method that may actually fail. The following listing shows new test method:

    /**
     * Ensures that there is no access between non-public parts of modules.
     * Each ArchUnit slice consists of the non-public part of each module.
     * There should be no dependencies between slices.
     */
    @Test
    public void accessRuleOneTest() {
        final JavaClasses theClassesToCheck = new ClassFileImporter()
            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
            .importPackages(APPLICATION_ROOT_PACKAGE);

        /* Maps Java classes to slices specifying which classes belong to a slice. */
        final SliceAssignment theClassToModulesSliceMapper = new ModulesSliceAssignment();

        SlicesRuleDefinition.slices()
            .assignedFrom(theClassToModulesSliceMapper)
            .should()
            .notDependOnEachOther()
            .because("this violates module encapsulation")
            .check(theClassesToCheck);
    }

Note that:

  • As earlier, a ClassFileImporter is used to obtain information on the classes in the application to check.
    Again it is the classes in the root package of the application that are being examined. Classes in JAR-files are filtered out.
  • An instance of the ModulesSliceAssignment class developed earlier is created.
  • ArchUnits fluent API is used to create slices using the ModulesSliceAssignment instance that should not depend on each other because this violates module encapsulation.
  • Finally the classes of the application are checked.

When run, the test will fail and produce the following log:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'slices assigned from non-public parts of modules should not depend on each other, because this violates module encapsulation' was violated (1 times):
Slice shoppingcart depends on Slice warehouse:
Field <se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest.mProductRepository> has type <se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductRepository> in (ShoppingCartServiceImplementationTest.java:0)
Field <se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest.mProductReservationsRepository> has type <se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductReservationRepository> in (ShoppingCartServiceImplementationTest.java:0)
Method <se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest.addItemEntireStockToCartTest()> calls method <se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductReservationRepository.findAllByProductNumber(java.lang.String)> in (ShoppingCartServiceImplementationTest.java:87)
Method <se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest.addItemEntireStockToCartTest()> calls method <se.ivankrizsan.monolithmicroservices.modules.warehouse.domain.ProductReservation.getReservedAmount()> in (ShoppingCartServiceImplementationTest.java:91)
Method <se.ivankrizsan.monolithmicroservices.modules.shoppingcart.implementation.ShoppingCartServiceImplementationTest.addItemAmountNotInStockToCartTest()> calls method <se.ivankrizsan.monolithmicroservices.modules.warehouse.persistence.ProductReservationRepository.findAllByProductNumber(java.lang.String)> in (ShoppingCartServiceImplementationTest.java:116)
	at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
	at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:86)
	at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:165)
	at com.tngtech.archunit.library.dependencies.SliceRule.check(SliceRule.java:81)

The test reveals that there are dependencies from the ShoppingCartServiceImplementationTest, being located in the shoppingcart module, to classes in the warehouse module, ProductRepository and ProductReservationRepository. I have apparently made the mistake of introducing dependencies to internal parts of the warehouse module from the test class in the shoppingcart module.
If all the code in the ShoppingCartServiceImplementationTest class is commented-out and the test is then re-run, it will now pass. This is of course just a quick and dirty fix to show that the test does pass if the cause to its failure is removed.

Access Rules Two and Three

To reiterate access rules two and three with clarifications:

  • Purple artifacts are not allowed to access red artifacts.
    Application code not belonging to any module may not access non-public parts of modules.
  • Red and green artifacts are not allowed to access purple artifacts.
    Code in modules, both public and non-public, are not allowed to have dependencies to application code not belonging to any module.

If the application is divided into three layers the predefined layered architecture in ArchUnit (link to LayeredArchtecture in the API documentation) may be useful to enforce access rules two and three.

The layers are as follows:

  • Public Module
    Parts of modules that may be accessed from other modules and external, non-module, code.
  • Non-public Module
    Parts of modules which may not be accessed from anywhere but from the public parts of the same module.
  • Non-module
    Application code not belonging to any module.

Using the names of the layers defined above, the access rules two and three now translates into:

  • Purple artifacts are not allowed to access red artifacts.
    Non-public Module artifacts may not be accessed by Non-module artifacts.
  • Red and green artifacts are not allowed to access purple artifacts.
    Non-public Module and Public Module artifacts are not allowed to access Non-module artifacts.
Figure showing the layers of the example application and allowed access between layers.
Figure showing the layers of the example application and allowed access between layers.

Note that:

  • Access rule one, which disallows access between non-public parts of different modules, will not be detected by the above.
    A test that enforces access rule one has already been developed.
  • Access from a public part of one module to a non-public part of another module will not be detected by the above.
    This case will be handled later in this article.

Non-Public Module Layer

In order to be able to determine whether an application adheres to a layered architecture the contents of each layer needs to be specified. For this a number of predicates are defined, one for each layer. In this example, I have chosen to define such predicates in separate methods. For the non-public module layer the predicate-method looks like this:

    /**
     * Creates an ArchUnit predicate that will select classes that:
     * - Are located in a package at least one level below the 'modules' package.
     *   That is, are located in a module.
     * - Are not located in a package named 'api' or 'configuration' in a module.
     *
     * @return Predicate that will select non-public module classes.
     */
    private DescribedPredicate<JavaClass> createModulesNonPublicClassesPredicate() {
        final DescribedPredicate<JavaClass> theResideInModulePredicate =
            JavaClass.Predicates.resideInAPackage("..modules.(*)..");
        final DescribedPredicate<JavaClass> theModulePublicPredicate =
            JavaClass.Predicates.resideInAPackage("..modules.(*).[api|configuration]..");
        final DescribedPredicate<JavaClass> theNotModulePublicPredicate =
            DescribedPredicate.not(theModulePublicPredicate);
        return theResideInModulePredicate.and(theNotModulePublicPredicate);
    }

In words the above predicate will match classes in modules, using the expression “..modules.(*)..”, and exclude classes in the api and configuration packages inside modules, using the expression “..modules.(*).[api|configuration]..”.

Public Module Layer

The predicate that defines the contents of the public module layer is much simpler than the above predicate since it just needs to match all classes inside the api and configuration packages in modules.

    /**
     * Creates an ArchUnit predicate that will select public classes in modules.
     *
     * @return Predicate that will select public module classes.
     */
    private DescribedPredicate<JavaClass> createModulesPublicClassesPredicate() {
        return JavaClass.Predicates.resideInAPackage("..modules.(*).[api|configuration]..");
    }

Non-Module Layer

The predicate that defines the content of the non-module layer is simply a negated predicate that defines the content of all modules.

    /**
     * Creates an ArchUnit predicate that will select classes that does not belong to any module.
     *
     * @return Predicate that will select non-module classes.
     */
    private DescribedPredicate<JavaClass> createNonModulesClassesPredicate() {
        final DescribedPredicate<JavaClass> theResideInModulePredicate =
            JavaClass.Predicates.resideInAPackage("..modules.(*)..");
        return DescribedPredicate.not(theResideInModulePredicate);
    }

Access Rules Two and Three Test

With the above predicates in place the test can be implemented.

    /**
     * Ensures that:
     * <li>Application code not belonging to any module may not have dependencies to non-public parts of modules.</li>
     * <li>Code in modules are not allowed to have dependencies to application code not belonging to any module.</li>
     *
     * The application code is divided in three layers:
     * <li>Public Module - Parts of modules that may be accessed from other modules and external, non-module, code.</li>
     * <li>Non-public Module - Parts of modules that may not be accessed from anywhere but the public part of the same module.</li>
     * <li>Non-module - Code outside of modules</li>
     *
     * <br/><b>Note!</b> No control of access between non-public parts of modules is performed - this is verified in the
     * {@code noAccessBetweenNonPublicPartsOfModulesTest}.<br/>
     * In addition, no control is made that all dependencies from public parts of modules to non-public parts
     * of modules are within one and the same module. That is a dependency from the public part of a module
     * to the non-public part of a module is not allowed to cross module boundaries.
     */
    @Test
    public void accessRulesTwoAndThreeTest() {
        final JavaClasses theClassesToCheck = new ClassFileImporter()
            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
            .importPackages(APPLICATION_ROOT_PACKAGE);

        Architectures
            .layeredArchitecture()
            .consideringAllDependencies()
            .layer("PublicModule").definedBy(createModulesPublicClassesPredicate())
            .layer("NonPublicModule").definedBy(createModulesNonPublicClassesPredicate())
            .layer("NonModule").definedBy(createNonModulesClassesPredicate())
            .whereLayer("PublicModule").mayOnlyBeAccessedByLayers("NonModule", "NonPublicModule")
            .whereLayer("NonPublicModule").mayOnlyBeAccessedByLayers("PublicModule")
            .whereLayer("NonModule").mayNotBeAccessedByAnyLayer()
            .check(theClassesToCheck);
    }

Note that:

  • A layered architecture that considers all dependencies when checking for violations.
    This configuration will consider dependencies to library classes and classes in Java.
  • Three layers, PublicModule, NonPublicModule and NonModule, are created and defined using the predicates created earlier.
  • A rule stating that the layer PublicModule may only be accessed from the layers NonModule and NonPublicModule is created.
    This allows access to the public parts of modules from all code in the application.
  • A rule stating that the layer NonPublicModule may only be accessed from the layer PublicModule is created.
    It shall be noted that this rule will not prevent access from the public parts of one module to the non-public parts of another module!
  • Finally a rule stating that the layer NonModule may not be accessed from any layer.
    This means that there must be no dependencies from code in modules to application code outside of modules.

Running the above test it should pass given the current state of the example application.

Access Rule Three Revisited

As stated earlier, access from the public part of one module to a non-public part of another module is not detected by the tests implemented so far. The final task, as far as ArchUnit tests are concerned, is to implement a tests that covers this case. For this, I have chosen to implement a custom ArchUnit rule consisting of a predicate and a condition. The predicate is used to select only classes located in public parts of modules and the condition then determines whether such a class have any dependencies to non-public classes in other modules.

Module Public Predicate

The implementation of the predicate that determines whether a class is located in a public part of a module utilizes the class ArchUnitModuleUtils implemented earlier making the implementation almost trivial:

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;        

import java.util.Optional;

/**
 * Described predicate that determines whether a class is located in a public part of a module.
 *
 * @author Ivan Krizsan
 */
public class ModulePublicDescribedPredicate extends DescribedPredicate<JavaClass> {

    /**
     * Creates an instance of the described predicate with the supplied parameters.
     *
     * @param inParameters Optional parameters.
     */
    public ModulePublicDescribedPredicate(final Object... inParameters) {
        super("are located in a public part of a module", inParameters);
    }

    @Override
    public boolean test(final JavaClass inJavaClass) {
        final Optional<String> theModuleNameOptional = ArchUnitModuleUtils.moduleFromJavaClass(inJavaClass);
        final boolean theModuleIsPublicFlag = ArchUnitModuleUtils.isLocatedInModulePublic(inJavaClass);

        return theModuleIsPublicFlag && theModuleNameOptional.isPresent();
    }
}

The above predicate consider a class to be located in the public part of a module if it is possible to obtain the name of the module and if the class is considered to be located in the public part of a module by the ArchUnit Module Utilities implemented earlier.

Has Dependency To Other Module Non-public Condition

Instances of JavaClass as created by ArchUnit contains, among other things, information about the dependencies of the class in question. The ArchUnit condition implemented here examines all the outgoing dependencies of a class and examines whether they are located in another module and, if this is the case, whether they are located in a non-public part of a module. The implementation of the ArchUnit condition also uses the ArchUnit Module Utilities implemented earlier.

package se.ivankrizsan.monolithmicroservices;

import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import lombok.extern.slf4j.Slf4j;

import java.util.Optional;

/**
 * An ArchUnit condition that checks whether classes have dependencies to non-public parts
 * in other modules.
 *
 * @author Ivan Krizsan
 */
@Slf4j
public class HasDependencyToOtherModulesNonPublicArchCondition extends ArchCondition<JavaClass> {

    /**
     * Creates an instance of the arch condition with the supplied additional parameters.
     *
     * @param inParameters Additional parameters.
     */
    public HasDependencyToOtherModulesNonPublicArchCondition(final Object... inParameters) {
        super("have dependencies to non-public classes in other modules", inParameters);
    }

    @Override
    public void check(final JavaClass inJavaClassToCheck, final ConditionEvents inConditionEvents) {
        log.debug("Checking Java class: {}", inJavaClassToCheck.getName());

        final Optional<String> theSourceModuleOptional = ArchUnitModuleUtils.moduleFromJavaClass(inJavaClassToCheck);
        if (theSourceModuleOptional.isPresent()) {
            final String theSourceModule = theSourceModuleOptional.get();

            for (Dependency theDestinationDependency : inJavaClassToCheck.getDirectDependenciesFromSelf()) {
                final JavaClass theDestinationClass = theDestinationDependency.getTargetClass();
                final boolean theDestinationModuleIsPublic = ArchUnitModuleUtils.isLocatedInModulePublic(theDestinationClass);
                final Optional<String> theDestinationModuleOptional = ArchUnitModuleUtils.moduleFromJavaClass(theDestinationClass);

                if (!theDestinationModuleIsPublic && theDestinationModuleOptional.isPresent()) {
                    final String theDestinationModule = theDestinationModuleOptional.get();
                    if (!theSourceModule.equalsIgnoreCase(theDestinationModule)) {
                        final String theViolationMessage = String.format(
                            "The class %s in the module '%s' has a dependency to the class %s in the module '%s', "
                                + " which is a non-public class in another module.",
                            inJavaClassToCheck.getName(),
                            theSourceModule,
                            theDestinationClass.getName(),
                            theDestinationModule);
                        inConditionEvents.add(SimpleConditionEvent.satisfied(inJavaClassToCheck, theViolationMessage));
                    }
                }
            }
        } else {
            log.warn("Java class '{}' is not located in a module", inJavaClassToCheck);
        }
    }
}

A warning is logged if the class examined is not located in a module since, in such a case, there may be a problem with the predicate implemented earlier or another, inappropriate, predicate has been used to filter the classes.

Access Rule Three Test

With the predicate and condition classes in place, the test can now be implemented as follows:

    /**
     * Ensures that no there are no dependencies from classes located in public part of modules
     * to classes located in non-public parts of other modules.
     * This test is a complement to the {@code accessRulesTwoAndThreeTest} as far as access rule three
     * is concerned.
     */
    @Test
    public void accessRuleThreeTest() {
        final JavaClasses theClassesToCheck = new ClassFileImporter()
            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
            .importPackages(APPLICATION_ROOT_PACKAGE);

        final ModulePublicDescribedPredicate resideInModulePublicPart = new ModulePublicDescribedPredicate();
        final ArchCondition<JavaClass> dependOnClassesInAnotherModulesNonPublicPart =
            new HasDependencyToOtherModulesNonPublicArchCondition(
                "has dependency to class in other module's non-public part");

        ArchRuleDefinition
            .noClasses()
            .that(resideInModulePublicPart)
            .should(dependOnClassesInAnotherModulesNonPublicPart)
            .check(theClassesToCheck);
    }

As can be seen, the predicate and condition classes makes it possible to write tests that are easy to read using ArchUnit´s fluent API.

Running the test in the current incarnation of the example program produces the following console output and the tests passes:

20:40:16 Checking Java class: se.ivankrizsan.monolithmicroservices.modules.warehouse.api.WarehouseService
20:40:16 Checking Java class: se.ivankrizsan.monolithmicroservices.modules.shoppingcart.api.ShoppingCartService
20:40:16 Checking Java class: se.ivankrizsan.monolithmicroservices.modules.shoppingcart.configuration.ShoppingCartConfiguration
20:40:16 Checking Java class: se.ivankrizsan.monolithmicroservices.modules.warehouse.configuration.WarehouseConfiguration

We see that only classes in the public parts, the api and configuration packages, of modules are checked by the condition-class.

Exercises

To verify that the implemented tests really catches violations one can now implement code that violates any of the access rules. An example of such an implementation is a default method in the WarehouseService interface that takes a parameter of the type ShoppingCartServiceImplementation. If the tests in ArchUnitTests are then run then the accessRuleThreeTest will fail.
Further experiments in this area are left as an exercise to the reader.

Final Words

I have come to the end of this first article on building a micro-service ready monolith in which the main part of the article was spent on examining ArchUnit as a means to enforce the suggested architecture.

My personal conclusion is that ArchUnit has indeed proven to be useful with the caveat that there is a learning-curve and that one has to be prepared to invest some time in creating ArchUnit tests. There is a better return-of-investment if the tests can be used in more than one project and, in such a case, one may want to create a library containing the tests that then can be included in the different projects.

One should remember that ArchUnit is only able to discover dependencies between Java classes and thus not dependencies from resources, such as XML configuration or similar, to Java classes.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *