Hexagonal Architecture is a powerful design pattern for modern backend systems. It completely isolates your core business logic from the outside world. It prevents frameworks, databases, and web APIs from leaking into your domain. However, protecting this architecture over time is very difficult. A single developer mistake can break the architectural boundaries and ruin the design.
When you build a large application, architectural rules often degrade. A developer might accidentally inject a Spring Framework component directly into a pure Domain entity. They might connect a web controller straight to a database repository, bypassing the correct ports. Manual code reviews are slow, and human reviewers often miss these structural violations. Over time, your clean architecture becomes a messy ball of mud.
The solution is not more documentation. The solution is automation. You need a way to verify your architecture on every build. In this guide, we will learn how to use ArchUnit in Spring Boot 4 to write unit tests that programmatically enforce Hexagonal Architecture. If someone breaks a rule, the build fails instantly.

The Problem with Manual Architectural Reviews
Many teams rely entirely on senior engineers to review pull requests and find architectural mistakes. This process has several major flaws:
- Review Fatigue: Reviewers get tired. When looking at hundreds of lines of code, it is easy to miss a bad import statement that breaks a boundary.
- Inconsistent Enforcement: Different reviewers have different opinions. One reviewer might reject a shortcut, while another reviewer might approve it to save time.
- Delayed Feedback: Developers wait days for a code review, only to find out they must rewrite their entire feature because it violates the architecture.
- Knowledge Silos: Only a few senior developers truly understand the architectural rules. When they leave the company, the rules disappear with them.
To scale a software engineering team, you must codify your rules. You must turn architectural diagrams into executable code. This is exactly what ArchUnit does for Java applications.
What is ArchUnit?
ArchUnit is a free, simple, and powerful Java testing library. It allows you to analyze your compiled Java bytecode and write rules using a fluent API. Instead of testing if a method returns “Hello World,” you test the physical structure of your code.
For example, you can write a test that says: “Classes in the Domain package must not depend on classes in the Web package.”
ArchUnit integrates seamlessly with JUnit 5. It runs incredibly fast because it analyzes bytecode, not source code. It acts as an automated shield for your project. If you are building large systems, you can also combine ArchUnit with Spring Modulith to enforce boundaries between different business domains.

Setting Up ArchUnit with Spring Boot 4
Adding ArchUnit to a Spring Boot 4 project is very easy. First, you need to add the ArchUnit JUnit 5 dependency to your pom.xml file. Because it is a testing library, ensure you set the Maven scope to test.
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
Next, you create a test class in your src/test/java directory. You use the @AnalyzeClasses annotation to tell ArchUnit which base package it needs to scan. This is the root package of your application.
package com.webcodein.workshop.architecture.hexagonal;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "com.webcodein.workshop.architecture.hexagonal")
public class ArchitectureTest {
// We will define our rules here
}
Enforcing the Golden Rule: Domain Purity
The most important rule in Hexagonal Architecture is that the Domain layer must remain absolutely pure. It must contain only standard Java code. It cannot depend on Spring Framework annotations (like @Service or @Autowired), database annotations (like @Entity), or web components (like @RestController).
Let us write an ArchUnit rule to enforce this domain purity. We will specify that no class inside the domain package can depend on the application package, the adapter package, or the org.springframework package.
@ArchTest
static final ArchRule domain_should_not_depend_on_outside =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..application..",
"..adapter..",
"org.springframework.."
);
If a junior engineer accidentally imports a JPA @Entity annotation into a Domain object, this test will fail immediately. They will receive a clear error message in their terminal.

Enforcing Ports and Adapters Rules
Another common mistake in modern architectures is Adapters talking directly to each other. For example, a REST Web Controller (an input adapter) should never call a Database Repository (an output adapter) directly. It must go through an Application Service (the input port).
We can write an ArchUnit rule to guarantee that input adapters never bypass the core logic.
@ArchTest
static final ArchRule adapters_should_not_depend_on_each_other =
noClasses().that().resideInAPackage("..adapter.in..")
.should().dependOnClassesThat().resideInAPackage("..adapter.out..");
If you want a more comprehensive check, ArchUnit provides a built-in shortcut for Onion Architecture. Onion Architecture perfectly maps to Hexagonal layers. You can define your entire structural diagram in one fluent block of code.
@ArchTest
static final ArchRule hexagonal_architecture_is_respected =
onionArchitecture()
.domainModels("..domain..")
.domainServices("..domain..")
.applicationServices("..application..")
.adapter("web", "..adapter.in.web..")
.adapter("persistence", "..adapter.out.persistence..");
This single rule is incredibly powerful. It verifies that your web adapters only talk to application services, your persistence adapters implement the correct output ports, and your domain remains isolated at the center.
Real-World Use Cases for ArchUnit
Beyond Hexagonal Architecture, you can use ArchUnit to enforce many practical coding standards in your enterprise Spring Boot applications.
1. Enforcing Naming Conventions
You can verify that all classes in a specific package follow a strict naming convention. For example, all classes inside the web adapter package must end with “Controller”.
@ArchTest
static final ArchRule controllers_must_have_controller_suffix =
classes().that().resideInAPackage("..adapter.in.web..")
.and().areAnnotatedWith(RestController.class)
.should().haveSimpleNameEndingWith("Controller");
2. Preventing Deprecated API Usage
If your team is migrating to a new internal library, you can write an ArchUnit rule that prevents any new code from calling deprecated methods or classes.
3. Securing Database Transactions
You can ensure that database transactions only start at the Application Service layer, and never inside the Web Controller layer. You can write a rule that forbids the use of @Transactional on web controllers.
If you have highly specific needs, you can even learn how to write custom ArchUnit rules to fit your exact business requirements.

Automating Architecture in CI/CD
Writing ArchUnit tests is only the first step. The real magic happens when you integrate these tests into your Continuous Integration (CI/CD) pipeline. Because ArchUnit runs as a standard JUnit test, it executes automatically during the Maven test phase.
When a developer opens a Pull Request on GitHub, GitHub Actions will compile the code and run the tests. If the developer breaks a Hexagonal Architecture rule, the pipeline turns red. The Pull Request is blocked. The code cannot be merged into the main branch.

This automated gatekeeper removes the emotional burden from code reviews. Developers do not argue with reviewers; they simply fix the code to pass the automated test. If you are also running advanced database tests, you can run ArchUnit alongside Testcontainers for a complete validation suite.
Advantages and Limitations
Before you adopt ArchUnit, you must understand both its strengths and its weaknesses.
Advantages
- Instant Feedback: Developers know immediately if they break the architecture. They do not have to wait days for a manual code review.
- Executable Documentation: Your ArchUnit rules serve as living documentation. New team members can read the test file to understand the architecture instantly.
- Zero Architectural Debt: Because the CI/CD pipeline blocks violations, your codebase stays clean forever.
- Freezing Legacy Code: If you add ArchUnit to an old, messy project, you can use the
FreezingArchRule. This ignores existing violations but strictly fails the build on any new mistakes.
Limitations
- Learning Curve: The fluent API is very powerful, but it takes time to master. Writing complex custom conditions can be difficult for beginners.
- Reflection Limits: ArchUnit analyzes compiled bytecode. It cannot verify runtime behaviors or dynamic configurations that happen when Spring Boot starts.
- False Positives: Sometimes, you might have a valid reason to break a rule. You must use the
@IgnoreArchTestannotation carefully, or your tests will become annoying blocks.
Best Practices and Common Mistakes
To get the most out of ArchUnit, you should follow these industry best practices and avoid common traps.
Do Not Over-Engineer Your Rules
A common mistake is writing too many small, strict rules. If you write a rule for every minor detail, developers will become frustrated. They will spend more time fixing tests than writing features. Focus only on the critical boundaries: Domain isolation, port interfaces, and adapter separation.
Keep the Rules Centralized
Do not scatter ArchUnit tests across dozens of files. Keep a single, central ArchitectureTest.java file at the root of your test directory. This makes it easy for developers to find and read the rules.
Use Clear Error Messages
When an ArchUnit test fails, the default error message can sometimes be long and confusing. You can use the because() method to add a custom, human-readable message.
@ArchTest
static final ArchRule domain_must_be_pure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..")
.because("The domain layer must remain pure and independent of the Spring Framework.");
Frequently Asked Questions (FAQ)
Can ArchUnit test Kotlin applications?
Yes, ArchUnit can analyze any code compiled to Java bytecode, including Kotlin. However, because Kotlin generates extra synthetic methods in the bytecode, you might need to adjust some rules to ignore synthetic Kotlin classes.
Does ArchUnit slow down the Maven build?
No. ArchUnit is incredibly fast. It uses reflection to scan the bytecode. For a medium-sized Spring Boot application, an ArchUnit test suite typically finishes in less than two seconds.
How do I fix an existing project with hundreds of violations?
Do not try to fix everything at once. Use the FreezingArchRule feature. This feature records all current violations in a text file and ignores them. However, it will fail the build if a developer introduces a brand new violation. Over time, you can fix the old violations one by one.
Conclusion
Hexagonal Architecture is a brilliant pattern, but it only works if the team strictly respects the boundaries. Manual code reviews are not enough to protect a growing codebase. By integrating ArchUnit into your Spring Boot 4 project, you eliminate human error and automate architectural enforcement.
ArchUnit acts as an invisible shield around your core business logic. It provides instant feedback, creates living documentation, and guarantees zero architectural debt. It is an absolute necessity for any serious Java engineering team building enterprise software.
Companion Code
You can find the complete, runnable source code for this article in the official companion repository. It includes the complete ArchUnit test suite and the Spring Boot 4 Hexagonal project. Clone the repository and run the tests yourself to see ArchUnit in action.
