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.
| 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 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.
Related deep dive: INP e Core Web Vitals: da 59 a 88 con Quattro Fix Angular SSR.
Commenti
Caricamento commenti...
Sign in to comment