ArchUnit

2022 09 01 head

How often have you experienced a well-defined and understood software architecture on paper? And it falls apart when developers start implementing it?

While re-architecting recently legacy components in an application, I experienced the same.

As more and more developers joined the team, it became a constant routine to make them aware of the design and how to adhere to it.

I know some of you may say, Why not control the implementation during code-review sessions?. Technically you can, but in that case, the reviewer becomes the bottleneck in the whole SDLC process.

What if there was something that could enforce design constraints in the form of the test cases? The violation of the agreed-upon design principles is marked as a failed build.

My quest led me to a test library for architecture called ArchUnit. It enforces architectural constraints in Java projects.

Idea

ArchUnit is a test library and allows us to validate whether a solution adheres to a given set of design considerations or architecture rules.

The product is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework. The license is the open source Apache 2.0. The support for JUnit 5 is very good.

ArchUnit can check dependencies between packages and classes, layers and slices, detect cyclic dependencies, and more. It does so by analyzing the given Java bytecode after importing all classes into a Java code structure.

Use Architectural Constraints

You first need to import the library in Gradle.

implementation com.tngtech.archunit:archunit:1.0.0                             (1)
1 For real projects you should describe your dependencies in a catalog file.

The definition of a validation test is easy.

Below an architectural rule to respect the Domain-Driven Design concepts in our project [1].

@AnalyzeClasses(packages = "net.tangly.erp.collaborators")                     (1)
public class DomainRules {                                                     (2)
    static final String SERVICES = "Services";
    static final String PORTS = "Ports";
    static final String DOMAIN = "Domain";

    @ArchTest
    static final ArchRule layersRule = layeredArchitecture().consideringAllDependencies()
        .layer(DOMAIN).definedBy("..domain..")
        .layer(SERVICES).definedBy("..services..")
        .layer(PORTS).definedBy("..ports..")
        .whereLayer(DOMAIN).mayOnlyBeAccessedByLayers(SERVICES, PORTS)
        .whereLayer(SERVICES).mayOnlyBeAccessedByLayers(PORTS)
        .whereLayer(PORTS).mayNotBeAccessedByAnyLayer();
}
1 Declare the root package for the bounded domain classes. The bounded domain should be defined as a Java module.
2 The domain rules class shall be declared as part of the domain library of your application.

These rules are defined in the library component and enforced in all the Bounded Domains we implement through the following unit tests.

@AnalyzeClasses(packages = "net.tangly.erp.collaborators")
public class ArchitectureTest {                                                (1)
    @ArchTest
    static final ArchTests domainRules = ArchTests.in(DomainRules.class);      (2)
}
1 The class is located in the unit test folder and will be executed with JUnit 5 as part of the unit test CI step.
2 Import all the rules defined in the domain rules class and apply them to the bounded domain collaborators.

We can insure architectural constraints to all bounded domains defined in a product. The constraints are defined only once in a common module.

The huge advantage is the integration of architectural validations in the continuous integration pipeline [2] [3]. No expensive human activities are required to enforce these rules [4].