Building Better Software with ArchUnit

Abdulqader Bazuhair
EGYM Software Development
4 min readSep 14, 2023

--

As developers, we often struggle with large legacy projects that can feel like navigating a maze. These projects start simple, but as different people or teams add features over time, they become complex puzzles. This is where ArchUnit can be incredibly helpful! ArchUnit defines clear architectural rules that should be followed, which can prevent these situations to some degree.

What is ArchUnit

ArchUnit is a handy Java library that works like a testing tool. It assists in ensuring that our application follows the architectural rules we’ve set.

But what are architectural rules? Think about them as the building blocks that shape how your codebase is built. Or a guideline on how the different parts of your code should interact with each other.

ArchUnit features

ArchUnit has several interesting features, such as

Automated Testing:

ArchUnit integrates with popular testing frameworks like JUnit. You can run tests by simply annotating @RunWith(ArchUnitRunner.class). These tests will scan your codebase and identify any violations of your architectural rules

Readable and Expressive Rules:

When we write code, it’s very important for the whole development team, that the code is clear and easy to understand. ArchUnit’s rules are created with this in mind, so even if you’re not a coding expert, your code will follow the rules in a way that everyone can understand. For example:

  noClasses().that().resideInAPackage("..repository..")
.should().dependOnClassesThat()
.resideInAnyPackage("..controllers..");

Getting Started with ArchUnit

Adding a dependency to your project

 dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit:version of junit you’re using ’
}

Importing Classes:

This will import the classes of your projects that need to be tested

JavaClasses classes = new ClassFileImporter().importPackages("com.egym.myapp");

Set Up Rules:

For example, let’s say you want to restrict access to the service layer to only controllers:

ArchRule accessRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..”)

Run the Check:

accessRule.check(classes);

Together, it looks like this:

@Test
public void Services_should_only_be_accessed_by_Controllers() {
JavaClasses classes = new ClassFileImporter().importPackages("com.egym.myapp");
ArchRule accessRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..”)
accessRule.check(classes);

}

Real-world Use Cases:

Dependency Checks:

ArchUnit can help to check if the class/package has the right dependencies

classes()
.that().resideInAPackage("..controller..")
.should().onlyDependOnClassesThat()
.resideInAPackage("..service..");
classes().that().haveNameMatching(".*Bar")
.should().onlyHaveDependentClassesThat().haveSimpleName("Bar")

Enforcing Naming Conventions:

For example, all the dto classes should have a suffix of “dto”

 classes()
.that().resideInAPackage("example.dto")
.should().haveSimpleNameEndingWith("dto")

Another example: the “service” package should exclusively contain classes designated as either “Service” or “Component.”

.that().resideInAPackage("example.service")
.should().haveSimpleNameEndingWith("Service")
.orShould().haveSimpleNameEndingWith("Component")

Annotation Checks:

Check if classes with certain annotations are in the right package

 classes()
.that()
.areAnnotatedWith(Transactional.class)
.should()
.resideInAPackage("..service..")

Layer Checks:

This rule enforces a strict separation of concerns, which helps to have better-structured architecture.

layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..repository..")

.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("repository").mayOnlyBeAccessedByLayers("Service")

Freezing Arch Rules:

When running ArchUnit in an existing project, it can identify numerous rule violations. However, addressing all these issues immediately might not be feasible, especially when the team is focused on delivering new features or under tight deadlines. ArchUnit provides an effective solution by enabling you to focus on testing only the recently added code. For the pre-existing code, a log file will document and monitor the locations where the rules are not met. This serves as a checklist for developers to follow. As soon as a fix is implemented, it will be automatically eliminated from the list during the next testing phase.

To make this function properly, we need to adjust the freeze.store.default property in the archunit.properties configuration

freeze.store.default.allowStoreCreation=true

Then, it can be used with any defined rule

ArchRule rule = FreezingArchRule.freeze(classes().should()…

For example, this test should check that all the classes in the service package should be annotated as Transactional

@Test
public void some_architecture_rule() {
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.ArchitectureDemo");

ArchRule accessRule = FreezingArchRule.freeze(classes()
.that().resideInAnyPackage("..service..")
.should().beAnnotatedWith(Transactional.class));
accessRule.check(importedClasses);
}

The test will not fail but will log all rule failures, serving as a developer to-do list.

Drawbacks

While ArchUnit offers several benefits, it also has some drawbacks and limitations:

Maintenance Overhead

As your project evolves, you may need to update the architectural rules enforced by ArchUnit to meet new requirements. This can add maintenance overhead.

Limited Language Support

ArchUnit is primarily designed for Java projects. This can be a limitation if the project stack includes multiple languages.

Summary

ArchUnit helps developers build solid code and architectures, navigate complex legacy projects, and maintain code quality. It is a valuable tool in software development, ensuring your codebase remains robust over time. However, it’s crucial to recognize its limitations and drawbacks when considering its use in your project.

--

--