Skip to main content

05 - Clean Architecture Spring Boot Template: the ArchUnit gate that genuinely runs

A Spring Boot 3.4 Clean Architecture template whose ArchUnit gate really runs four rules, extracted by hand from a production backend.

What do you think about this article?

05 - Clean Architecture Spring Boot Template: the ArchUnit gate that genuinely runs
Original Researchfedericocalo.dev

At a Glance

A Spring Boot 3.4 Clean Architecture template whose ArchUnit gate really runs four rules, extracted by hand from a production backend.

Direct answer

I extracted a Clean Architecture template from a production Spring Boot backend that mixes 25 proprietary business verticals, curating the files by hand instead of using git filter-repo. The result is a public 4-layer repo with JWT, Flyway, and an ArchUnit gate that genuinely runs four rules, green on JDK 21.

Most "Clean Architecture" templates on GitHub lie by omission: they show a pretty four-layer diagram in the README and an ArchUnit test in src/test, but that test runs zero rules and is green for nothing. An architecture diagram is worth nothing until a test forces it on every build. This template was born from exactly that lesson, learned in production the hard way.

The clean-architecture-spring-boot-template repo is a sanitized extraction of my portfolio's real backend: same stack (Spring Boot 3.4, Java 21, jjwt, Flyway, ArchUnit, Resilience4j), but stripped of every piece of business logic and every secret. It is not a skeleton invented for the occasion: it is the generic distillate of code that actually runs, with the single difference that the example aggregate is a trivial Task instead of the proprietary verticals.

Why I did not use git filter-repo

The temptation, when extracting a template out of a monorepo, is to automate: a git filter-repo --path domain --path application and done. In my case that would have been a disaster. The four packages domain, application, infrastructure and presentation are organized by layer, not by feature: inside domain the generic scaffolding coexists with roughly 25 business verticals (e-invoicing, tax compliance, EU AI Act modules, and more). A path filter would have dragged all that proprietary logic into a public repo.

There is a second, less obvious reason: git filter-repo performs a reset --hard on the current working tree, not just on the target clone. Running it in the live working directory, with uncommitted changes, means losing them. For a template the correct choice is manual, file-by-file curation into a fresh directory with a small, clean git history: no monorepo history preserved, therefore no risk of exposing content or comments that were removed in later commits but still survive in history.

The ArchUnit gate that genuinely runs (and not zero tests)

The heart of the template is the CleanArchitectureTest file. Its only reason to exist is to fail the build when someone violates the dependency direction: the arrow must point toward the domain (presentation, then application, then domain, with infrastructure depending on the domain, never the other way around). But here lies the trap that gave rise to the entire repo.

In the real backend, for months, the ArchUnit gate was green while protecting nothing. The reason: written with the idiomatic @AnalyzeClasses + static @ArchTest fields pattern, the archunit-junit5 engine was not discovered by Surefire against the JUnit Platform version that Spring Boot 3.4 brings along. Result: Tests run: 0. A green badge on a rule that was never executed. We only found the flaw by deliberately introducing a forbidden dependency and watching the build stay green.

Comparison between the static-field ArchUnit pattern and the explicit @Test methods pattern
ArchUnit test pattern Rules executed Outcome
@AnalyzeClasses + @ArchTest fields 0 green for nothing (protects nothing)
@Test + ClassFileImporter (this template) 4 real gate, fails on violations
The same rule set with two writing patterns: only the second is actually executed by Surefire with the Spring Boot 3.4 stack.

The fix is simple but not obvious: no @ArchTest fields. You import the classes once in @BeforeAll with ClassFileImporter, and each rule becomes an ordinary @Test method that calls rule.check(importedClasses). These Jupiter methods are discovered like any other unit test. The closing check is always the same: mvn test -Dtest=CleanArchitectureTest must print Tests run: 4, never Tests run: 0.

The declared trade-offs of a "broad" template

I chose a broad extraction instead of a minimal skeleton with a single slice. The cost is real: more surface to maintain, more dependencies (JWT, Flyway, Resilience4j, rate-limit) that a minimal project would not carry. The benefit is that the template shows how the layers interact in a non-trivial case, not just in a toy CRUD. A reader who only wants the pure structure can delete the JWT slice or the rate-limit in five minutes; the opposite, adding a realistic pattern to an overly bare skeleton, takes much more work and leaves doubts about how to wire it correctly against the layers.

The second trade-off is about secrets. The template ships a .env.example with placeholders and an application.yml that reads everything from environment variables, with defaults explicitly marked as non-production. No real HS256 signing key, no database credentials, no production IP: a grep-based secret-scan runs before the first push and must exit with code 0. It is a deliberately simple defense, not a vault: for a public template the threshold is "no real secret in the tree", not "advanced secret management".

Who it is for, and who it is not

If you are starting a new Spring Boot service and want to begin with layer separation already enforced by a test, this template saves you the boring, tricky part: wiring the ports, the persistence adapter that translates between domain and JPA, the stateless JWT filter, and above all the ArchUnit gate written the way that genuinely runs. If instead you are looking for an opinionated framework with code generation, CLI scaffolding and ready-made modules, this is not for you: it is deliberately small, readable in an afternoon, and meant to be read and modified, not consumed as a dependency.


In the "Original Research" series, previous: 04 - INP and Core Web Vitals: From 59 to 88.

Frequently asked questions

Why can an ArchUnit test with @ArchTest fields be green yet run nothing?

Because the archunit-junit5 engine is not discovered by Surefire against the JUnit Platform version Spring Boot 3.4 brings along: the static @ArchTest fields are not collected as tests and the result is Tests run: 0, a green that protects nothing. The template avoids the problem by using explicit @Test methods with ClassFileImporter, which are discovered like any other unit test.

Why did you not use git filter-repo to extract the template from the monorepo?

Two reasons. First: the monorepo layers mix generic scaffolding with roughly 25 proprietary business verticals in the same package hierarchy, so a path filter would have dragged proprietary code into a public repo. Second: git filter-repo performs a reset --hard on the current working tree, risking the destruction of uncommitted work. Manual file-by-file curation with a fresh git history is safer.

Does the template need a database to compile and pass its tests?

No. The domain tests and the ArchUnit gate do not load the Spring context, so mvn test passes on JDK 21 without a database and without a network. The Postgres database and the Flyway migration are only needed when you actually start the application with mvn spring-boot:run.

How can I be sure the gate is really running?

Run mvn test -Dtest=CleanArchitectureTest and check that the output reports Tests run: 4 (not Tests run: 0). As a counter-proof, deliberately introduce a forbidden dependency, for example import an infrastructure class inside a presentation controller: the build must fail. Then revert the change.

Sources and further reading

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects