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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Enforce Spring Boot Architecture Rules Automatically with ArchUnit

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootCode QualityLayered ArchitectureArchitecture TestingArchUnitAutomated GovernanceCyclic Dependencies
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.