Enforcing a specific package structure or architecture is very important. Especially in Java where some things must be public to work correctly or actually be available outside its package. ArchUnit is an open-source library that will help you whenever the compiler is not enough.
All of the code examples from this article is available in my GitHub repo.
What is ArchUnit?
ArchUnit is an open-source library for writing and enforcing architecture rules within your project. There is no use of facades and encapsulations when you can just reach out and take what you want. It is even more significant when you are trying to follow ports and adapters or other approaches that impose very strict restrictions on which classes should use others and in what way.
That is the moment where ArchUnit comes into play. It can easily check the dependencies between packages and classes — which class is using/importing the other. With this “simple” feature we can easily set up the set of rules that will put restrictions on how our classes can interact with one another. Later we can easily add these rules to our test suite and by extension unit test our architecture.
For all designs and purposes the rules are normal testes and can be easily run by any unit test library/framework. Beside “simple” packages and classes dependencies check mentioned above it can also check dependencies between layers and slices, check for cyclic dependencies and more.
We can create following rules with the help of ArchUnit
- “Classes in package X should only depend on classes in package Y.”
- “Classes in the service layer should not access controller layer classes.”
- “No cyclic dependencies should exist among these packages.”
- Prevent a field and setter based injection
- Ensure @Transactional annotation is used only in the service layer
- Enforce @Repository and @Service annotation usage in specific packages
- ….
Without going into much detail ArchUnit works by reading and analyzing bytecode not the source code itself. Thus, our rules are not applied to source code per se but rather to output bytecode.
ArchUnit-Junit
ArchUnit-Junit artifact is part of the wider ArchUnit framework. It makes the tests more descriptive and smaller by removing a lot of JUnit related boilerplate code. The most import part of this package is ArchTest
annotation. With using it we can write tests as methods not JUnit tests. The artifact will take care of actually converting the method into proper JUnit test.
ArchUnit-Junit
@ArchTest
static final ArchRule classesInXShouldOnlyDependOnClassesInY =
ArchRuleDefinition
.classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..",
"java.."
);
JUnit
@Test
void testClassesInXShouldOnlyDependOnClassesInY() {
ArchRule rule = classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..",
"java.."
);
rule.check(IMPORTED_CLASSES);
}
The difference is seem no a big deal, however I can see potential benefits if you have a lot of tests. Personally, I prefer the classic JUnit way.
All the tests written here follow a JUnit way without the archunit-junit5-engine
. Nevertheless, you can find the examples written with junit-archunit
lib in the repo.
ArchUnit Examples
Let’s start with implementation of all the rules from above. Then I will move on to presenting rules that will ensure your ports & adapters setup remains unchanged.
Classes in package X should only depend on classes in package Y.
@Test
void testClassesInXShouldOnlyDependOnClassesInY() {
// Given: Define a rule that restricts classes in package '..x..'
// to depend only on classes in '..y..' or standard Java packages.
ArchRule rule = classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..", // Allow dependency on package '..y..'
"java.." // Allow dependency on Java standard library
);
// Then
rule.check(IMPORTED_CLASSES);
}
Classes in the service layer should not access controller layer classes.
@Test
void testServiceLayerShouldNotAccessControllers() {
// Given: Define a rule that prevents the service layer
// from depending on classes in the controller layer.
ArchRule rule = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..");
// Then
rule.check(IMPORTED_CLASSES);
}
No cyclic dependencies should exist among packages.
@Test
void testNoCyclicDependencies() {
// Given: Define a rule to ensure there are no cyclic dependencies
// between modules grouped by their first-level sub-packages under 'org.ps'.
ArchRule rule = SlicesRuleDefinition.slices()
.matching("org.ps.(*)..") // Define slices by sub-packages under 'org.ps'
.should()
.beFreeOfCycles(); // Ensure there's no cyclic dependency between them
// Then
rule.check(IMPORTED_CLASSES);
}
Prevent the field and setter based
@Test
void testNoFieldInjection() {
// Given: Define a rule that disallows field injection using @Autowired.
ArchRule noFieldInjectionRule = noFields()
.should().beAnnotatedWith(Autowired.class)
.because("Use constructor injection instead of field injection.");
// Also define a rule that disallows setter injection using @Autowired.
ArchRule noSetterInjectionRule = noMethods()
.that().haveNameMatching("set[A-Z].*")
.should().beAnnotatedWith(Autowired.class)
.because("Use constructor injection instead of setter injection.");
// When: Combine both rules into one composite rule.
ArchRule compositeRule = CompositeArchRule.of(noFieldInjectionRule).and(noSetterInjectionRule);
// Then
compositeRule.check(IMPORTED_CLASSES);
}
Ensure @Transactional annotation is used only in the service layer.
@Test
void testTransactionalAnnotationOnlyInService() {
// Given: Define a rule that ensures classes annotated with @Transactional
// are located in the service layer.
ArchRule classLevelTransactional = classes()
.that().areAnnotatedWith(Transactional.class)
.should().resideInAPackage("..service..")
.because("Class-level @Transactional belongs in the service layer only.");
// Also define a rule for methods annotated with @Transactional
// to be declared only in service layer classes.
ArchRule methodLevelTransactional = methods()
.that().areAnnotatedWith(Transactional.class)
.should().beDeclaredInClassesThat().resideInAPackage("..service..")
.because("Method-level @Transactional belongs in the service layer only.");
// When: Combine both rules into one composite rule.
ArchRule compositeRule = CompositeArchRule.of(classLevelTransactional).and(methodLevelTransactional);
// Then
compositeRule.check(IMPORTED_CLASSES);
}
Enforce @Repository and @Service annotation usage in specific packages.
@Test
void testRepositoryAnnotationInRepositoryPackage() {
// Given: Define a rule that ensures @Repository-annotated classes
// are only located in the repository package.
ArchRule rule = classes()
.that().areAnnotatedWith(Repository.class)
.should().resideInAPackage("..repository..");
// Then
rule.check(IMPORTED_CLASSES);
}
@Test
void testServiceAnnotationInServicePackage() {
// Given: Define a rule that ensures @Service-annotated classes
// are only located in the service package.
ArchRule rule = classes()
.that().areAnnotatedWith(Service.class)
.should().resideInAPackage("..service..");
// Then
rule.check(IMPORTED_CLASSES);
}
ArchUnit & Hexagonal Architecture
Here the setup is somewhat more complex. A complete set of ArchUnit tests for Hexagonal architecture.
Due to Java packaging model, enforcing proper classes and methods visibility is sometimes impossible. Thus, someone may easily use the class outside its intended scope. By extend breaking the encapsulation and our beautiful separation of domain and infrastructure.
Let’s consider following setup:
org.ps
├─ domain
│ └─ ... (domain models and services)
├─ application
│ ├─ port
│ │ ├─ in
│ │ │ └─ ... (interfaces for incoming incoming requests and messages)
│ │ └─ out
│ │ └─ ... (interfaces for outgoing requests and messages)
│ └─ ... (application services, use case implementations)
├─ adapters
│ ├─ in (incoming requests and messages)
│ └─ out (outgoing requests and messages)
├─ infrastructure
│ └─ ... (external setups - DB connections, queues, metrics)
└─ config
└─ ... (configurations classes for all other packages)
This is the closest to recommended package structure for hexagonal architecture I managed to get. It seems that there are no one general way. Almost every article is pushing its one version.
We want our structure to obey following set of rules, as far as I have managed to understand industry-wide standard when it comes to hexagonal architecture:
- Domain may not access any layer but can be access by Application and Adapters layers.
- Application may access the Config and Domain layers but can be access by Adapters layer.
- Adapters may access Application, Adapters, Domain and Infrastructure by cannot be access by other layers.
- Infrastructure can only access Config layer but can be access only by Adapters.
- Config may not be access any layer but can be access by Application, Adapters, Domain and Infrastructure.
Below is how we may test and enforce it with the help of ArchUnit. The test is quite lengthy but particular rules are clearly split from one another.
@Test
public void hexagonArchTest() {
// Given
JavaClasses importedClasses = new ClassFileImporter().importPackages("org.ps.hexagon");
LayeredArchitecture portsAndAdaptersLayers = layeredArchitecture()
.consideringOnlyDependenciesInLayers()
// Define each “layer” by its package
.layer("Adapters").definedBy("..adapters..")
.layer("Application").definedBy("..application..")
.layer("Config").definedBy("..config..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
// Domain may not access any layer but can be access by Application and Adapters layers.
.whereLayer("Domain").mayNotAccessAnyLayer()
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Adapters")
// Application may access the Config and Domain layers but can be access by Adapters layer.
.whereLayer("Application").mayOnlyAccessLayers("Config", "Domain")
.whereLayer("Application").mayOnlyBeAccessedByLayers("Adapters")
// Adapters may access Application, Adapters, Domain and Infrastructure but cannot be access by other layers.
.whereLayer("Adapters").mayOnlyAccessLayers("Infrastructure", "Config", "Application", "Domain")
.whereLayer("Adapters").mayNotBeAccessedByAnyLayer()
// Infrastructure can only access Config layer but can be access only by Adapters.
.whereLayer("Infrastructure").mayOnlyAccessLayers("Config")
.whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Adapters")
// Config may not be access any layer but can be access by Application, Adapters, Domain and Infrastructure.
.whereLayer("Config").mayNotAccessAnyLayer()
.whereLayer("Config").mayOnlyBeAccessedByLayers("Application", "Adapters", "Domain", "Infrastructure");
// Then
portsAndAdaptersLayers.check(importedClasses);
}
Summary
Here we are, that is all I wanted to share with you today. If you want some more examples you can find them either on ArchUnit GitHub or in their docs.
On the other hand, you can just start typing and see where the API will guide you.
Thank you for your time.