ArchUnit for Spring Boot — Enforcing Layered Architecture as a Unit Test
Companion module for ArchUnit for Spring Boot: Enforcing Layered Architecture as a Unit Test on ankurm.com. Every code sample and every console transcript quoted in that post comes from the files in this directory — nothing was hand-typed into the article.
Versions this was built and run against
| Component | Version | Notes |
|---|---|---|
| ArchUnit | 1.5.1 | current GA per maven-metadata.xml on Maven Central at the time of writing |
| JUnit Jupiter / Platform | 6.1.3 | via archunit-junit5, which brings its own JUnit 5 extension |
| Spring Framework | 7.0.9 | spring-context + spring-webmvc only — no spring-boot-starter-web, no embedded server, no ApplicationContext anywhere in this module |
| JDK | 25 (Temurin, LTS) | build and run; maven.compiler.release is set to 17 |
| Maven | 3.9.11 | |
| Maven Surefire Plugin | 3.5.2 |
This module deliberately does not depend on any spring-boot-starter-* artifact. ArchUnit
reads compiled .class files directly — it has never needed a running Spring context, and
the whole point of the first section of the post is that none of these tests say
@SpringBootTest.
Quickstart
mvn test # all four rules, no application ever starts, ~4s total
mvn test -Dtest=LayeredArchitectureTest # just the three structural rules
mvn test -Dtest=FreezingArchRuleTest # just the frozen-violation rule
Gradle users: there is no build.gradle.kts in this module (unlike this repo's other
modules) because the whole demonstration is Maven-plugin-agnostic — archunit-junit5
plugs into the JUnit 5 engine the same way under either build tool, and the commands above
are the part that matters.
Source files
| File | Demonstrates |
|---|---|
OrderRepository.java / InMemoryOrderRepository.java |
the repository layer — a @Repository with no framework beyond the stereotype annotation itself |
OrderStatusService.java |
the service layer — constructor injection only, which is what makes noFieldInjection meaningful rather than cosmetic |
OrderStatusController.java |
the web layer — depends on the service layer only |
LegacyOrderExporter.java |
a deliberately-kept pre-existing throws Exception violation — the thing FreezingArchRuleTest freezes rather than fixes |
LayeredArchitectureTest.java |
layeredArchitecture(), noFields().should().beAnnotatedWith(Autowired.class), and slices().should().beFreeOfCycles() — three independent structural rules, all checked without starting anything |
FreezingArchRuleTest.java |
FreezingArchRule.freeze(...) wrapping a rule that would otherwise fail immediately, recording today's known violations as an accepted baseline |
archunit.properties |
the three properties that control where the frozen-violation store lives and whether it may be created or updated |
archunit_store/ |
the frozen-violation store itself, committed to version control — this directory is the baseline the rule enforces against |
Captured output
| File | What it shows |
|---|---|
00-baseline-passing-run.txt |
the three structural rules, all green, ~1.3s, no ApplicationContext started |
01-layering-violation-failure.txt |
a real layeredArchitecture() failure, captured by temporarily adding a controller that bypassed the service layer, then deleting it |
02-freeze-run1-baseline-captured.txt |
the first-ever run of FreezingArchRuleTest: no store existed yet, so it was created with today's one known violation already accepted |
03-freeze-run2-new-violation-caught.txt |
a second, genuinely new throws Exception added after the baseline was recorded — the failure names only the new offender, never the frozen one |
04-full-suite-final.txt |
all four rules, exactly as committed, green in under four seconds |
A diagnostic note, not a defect
LegacyOrderExporter.export() still declares throws Exception in the committed code, and
noNewGenericExceptionsDeclared in FreezingArchRuleTest still technically covers it. The
build is green anyway, on purpose: archunit_store/ records that specific violation as
already known at the time the rule was turned on, and freeze() only fails a build on a
violation that is new since that recording. This is the realistic case — a team adopting
ArchUnit on a codebase that is not already clean — and the point the post's freezing section
makes is that you do not have to choose between "fix two hundred existing violations today"
and "don't bother enforcing the rule at all."
License
MIT, matching the rest of this repository.