Enforce Spring Boot Architecture Rules Automatically with ArchUnit
This tutorial demonstrates how to use ArchUnit to automatically enforce architectural constraints in Spring Boot projects, covering package structure validation, layered architecture protection, cycle detection between modules, and blocking problematic dependencies like BigDecimal(double) constructor through automated tests.
Introduction
As Spring Boot projects grow, module dependencies become complex. Architecture issues often surface only after long-term iteration, making code hard to maintain and extend. ArchUnit addresses this by enabling automated architecture tests that continuously guard architectural rules, similar to unit tests.
What Is ArchUnit?
ArchUnit is a free, lightweight, and extensible Java library for verifying code architecture. It validates dependencies between packages and classes, checks layer and business module boundaries, and detects cyclic dependencies. It works by parsing Java bytecode, loading all classes, and building a complete code structure model. ArchUnit integrates with standard Java unit test frameworks (JUnit 4/5, TestNG) to automate architecture rule verification.
Quick Start
Maven Dependency
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>Gradle Dependency
testImplementation 'com.tngtech.archunit:archunit:1.4.2'Basic Test Class Structure
public class MyArchitectureTest {
@Test
public void some_architecture_rule() {
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.pack");
ArchRule rule = classes().that()
.areAnnotatedWith(Service.class)
.should().onlyBeAccessed().byAnyPackage("com.pack");
rule.check(importedClasses);
}
}The fluent API allows expressive rule definitions (see ArchUnit documentation for full API).
Package Structure Validation
The first high-impact rule typically enforces package naming conventions. For example, ensuring all classes ending with Service reside in a service package.
@AnalyzeClasses(packages = "com.pack")
class PackageRulesTest {
@ArchTest
static final ArchRule services_should_live_in_service_package = classes()
.that().haveSimpleNameEndingWith("Service")
.should().resideInAPackage("..service..");
}When a PaymentService is mistakenly placed in a web package, the test fails with a clear violation report listing the offending class.
Layered Architecture Protection
Many projects claim layered architecture but violate it in code (e.g., controllers calling repositories directly). ArchUnit provides dedicated layered architecture verification.
@AnalyzeClasses(packages = "com.pack")
class LayeredArchitectureTest {
@ArchTest
void layersCheck(JavaClasses importedClasses) {
Architectures.layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
.check(importedClasses);
}
}If a UserController directly injects UserRepository (bypassing the service layer), the test fails, showing the illegal dependency path.
Cycle Detection Between Business Modules
In Domain-Driven Design, business modules (slices) like user, order, goods should not have cyclic dependencies. ArchUnit can verify this using slice matching.
Example cyclic dependency: OrderService depends on UserService, and UserService depends on OrderService.
@AnalyzeClasses(packages = "com.pack")
class CycleRulesTest {
@ArchTest
static final ArchRule feature_slices_should_be_free_of_cycles =
slices().matching("com.pack.(*)..")
.should().beFreeOfCycles();
}The pattern com.pack.(*).. captures the first sub-package as the slice identifier (e.g., user, order) and includes all its sub-packages. The test fails if any cycles exist between slices.
Blocking Specific Problematic Dependencies
Beyond structural rules, ArchUnit can forbid specific API usages. A common example: prohibiting new BigDecimal(double) due to floating-point precision loss.
@AnalyzeClasses(packages = "com.pack")
class DependencyRulesTest {
@ArchTest
void noShouldCallCheck(JavaClasses classes) {
ArchRule rule = noClasses().should()
.callConstructor(BigDecimal.class, double.class);
rule.check(classes);
}
}If any class contains new BigDecimal(666D), the test fails, preventing a known source of calculation bugs.
Further Resources
Official ArchUnit user guide: https://www.archunit.org/userguide/html/000_Index.html#_importing_classes
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
