Salta al contenuto principale

ArchUnit e Clean Architecture: il Diagramma a 4 Layer è una Bugia Finché Non lo Forzi

Il gate ArchUnit del backend girava a zero test eseguiti, un falso verde totale. Come l'ho scoperto, cosa ho congelato con FreezingArchRule (30+4 violazioni storiche) e perché un diagramma a 4 layer senza enforcement automatico non è un'architettura.

Cosa ne pensi di questo articolo?

ArchUnit e Clean Architecture: il Diagramma a 4 Layer è una Bugia Finché Non lo Forzi
Architettura Softwarefedericocalo.dev

In Breve

Il gate ArchUnit del backend girava a zero test eseguiti, un falso verde totale. Come l'ho scoperto, cosa ho congelato con FreezingArchRule (30+4 violazioni storiche) e perché un diagramma a 4 layer senza enforcement automatico non è un'architettura.

Risposta diretta

Un diagramma a 4 layer (domain, application, infrastructure, presentation) non è un'architettura, è un'intenzione. Diventa un'architettura solo quando un gate automatico la fa rispettare. Nel backend di questo portfolio quel gate ha girato per un tempo non quantificabile con zero classi importate, un falso verde totale, prima di essere scoperto e sostituito da un ArchUnit test che oggi congela 30 violazioni storiche presentation-infrastructure e 4 di domain purity, bloccando ogni nuova regressione.

La maggior parte dei diagrammi di Clean Architecture che circolano nei talk e nei repository README sono opere di fiction aspirazionale. Le frecce puntano verso il dominio, i layer sono colorati in modo pulito, e nessuno ha mai verificato che il codice reale rispetti quelle frecce. Nel backend Spring Boot di questo portfolio (4 layer, oltre 3.100 file Java main) ho scoperto che il mio stesso gate di enforcement, CleanArchitectureTest, girava da tempo indeterminato eseguendo zero classi. Un test verde che non testava niente. Ecco cosa ho imparato forzando davvero l'architettura, non solo disegnandola.

Il layering è un vincolo, non un'estetica

La regola di dipendenza della Clean Architecture è semplice da enunciare: presentation → application → domain ← infrastructure. Il dominio non deve sapere nulla di Spring, di JPA, di HTTP. I controller non devono importare direttamente repository JPA o adapter di infrastruttura, devono passare per un caso d'uso applicativo. Sulla carta è ovvio. In pratica, con oltre 3.100 classi e decine di verticali di business (fiscale, ProjectHub, dataset marketplace, ecosystem portal, urbex) aggiunte in mesi diversi da agenti diversi, la regola si rompe silenziosamente decine di volte, perché è più veloce iniettare direttamente il repository che passare per un port applicativo, e niente lo impedisce, a meno che qualcosa lo verifichi ad ogni build.

Il falso verde: @ArchTest contro @Test

Il primo tentativo di enforcement usava le annotazioni @ArchTest su campi statici, il pattern "canonico" documentato da ArchUnit stesso. Il problema: il motore di discovery archunit-junit5 (nella versione usata) non viene rilevato da Surefire 3.5.3 contro JUnit Platform 1.11.4, gestito transitivamente da Spring Boot 3.4 tramite junit-jupiter 5.11.4. Risultato: Tests run: 0. Il test compilava, il modulo faceva build verde, la CI passava. Nessun errore, nessun warning visibile in output normale. Un gate di sicurezza architetturale che non proteggeva assolutamente nulla, per un periodo di tempo che non sono in grado di quantificare con precisione perché il sintomo non genera alcun segnale distintivo.

La scoperta è arrivata solo rieseguendo esplicitamente la classe e leggendo il numero di test eseguiti nell'output, non un errore rosso, un semplice zero dove ci si aspettava un numero maggiore di zero. Il fix: sostituire i campi @ArchTest con metodi ordinari @Test di JUnit Jupiter, importare le classi una volta in @BeforeAll con ClassFileImporter, e chiamare rule.check(imported) dentro ogni test. Lo stesso motore Jupiter che esegue già ogni altro test del modulo esegue anche questo, deterministicamente.

Congelare il debito invece di fingere che non esista

Riattivato il gate, la domanda immediata è stata: quante violazioni reali sono state introdotte durante il periodo di falso verde? La risposta onesta è stata scomoda: circa 30 classi presentation che importano direttamente infrastructure, e 4 classi domain che dipendono da Spring o JPA. Bloccare tutto subito avrebbe significato fermare ogni sviluppo per settimane su un refactoring enorme e a basso ROI immediato. La scelta è stata usare FreezingArchRule: congelare esplicitamente le violazioni storiche in uno store versionato (archunit-store), così il gate torna verde per il debito esistente, ma ogni nuova violazione aggiunta dopo il freeze fa fallire il build immediatamente.

Stato del gate ArchUnit prima e dopo il fix, backend Spring Boot
Gate Prima (falso verde) Dopo (frozen)
Classi importate dal test 0 3.100+
Violazioni presentation → infrastructure rilevate 0 (non verificate) 30 (congelate, no nuove)
Violazioni domain purity rilevate 0 (non verificate) 4 (congelate, no nuove)
Nuove violazioni ammesse dopo il freeze N/A 0
Audit interno del 2026-07-07 sul backend Spring Boot 3.4, 4-layer Clean Architecture. Codice del gate: CleanArchitectureTest.java.

Il trade-off è esplicito e va dichiarato, non nascosto: congelare il debito significa accettare che 34 classi restino non conformi a tempo indefinito, finché qualcuno non decide di rifattorizzarle volontariamente. Il beneficio è che il debito è ora tracciato, visibile, quantificato, invece di essere invisibile come lo era con il falso verde. Un debito visibile è un debito che qualcuno può decidere di pagare. Un debito invisibile è semplicemente un incidente di produzione differito.

Perché lo scope stretto ha battuto lo scope largo, all'inizio

Prima del gate globale con freezing, esistevano regole a scope stretto su singole classi o package (FiscalAiController, il package euinvoice.presentation). Queste regole narrow avevano un vantaggio pratico: zero falsi positivi su classi non correlate, feedback immediato e leggibile durante lo sviluppo incrementale di una feature specifica. Lo svantaggio: coprivano solo una minuscola frazione della codebase, lasciando fuori esattamente le 30+4 violazioni che il gate globale ha poi trovato. Il pattern che funziona in pratica non è "scope stretto O scope largo", è entrambi in sequenza: regole narrow durante lo sviluppo attivo di una feature, poi un gate globale con freeze che chiude il cerchio senza dover riscrivere ogni regola narrow.

Quando NON vale la pena forzare l'architettura

Onestamente: se il team è una persona sola, se il codice ha una vita attesa di poche settimane (prototipo, spike, proof of concept), o se il progetto ha meno di 50 classi, il costo di setup di ArchUnit e FreezingArchRule supera il beneficio. Il punto di rottura, nella mia esperienza diretta su questo backend, è arrivato tra le 500 e le 1.000 classi, quando iniziare a "ricordare a memoria" quali layer dipendono da quali è diventato impossibile con precisione, e le violazioni hanno iniziato a essere introdotte per distrazione, non per scelta consapevole.

La lezione che porto avanti

Un test che gira zero volte è più pericoloso di nessun test, perché dà falsa sicurezza. La lezione operativa concreta che applico ora a ogni nuovo gate ArchUnit: dopo aver scritto il test, lo eseguo esplicitamente una volta e leggo il numero di test effettivamente eseguiti nell'output, non solo il colore verde/rosso. Zero test eseguiti è un fallimento silenzioso, non un successo.


Domande frequenti

Perché un test ArchUnit può risultare verde senza verificare nulla?

Se il gate usa campi statici @ArchTest e il motore di discovery archunit-junit5 non è compatibile con la versione di Surefire/JUnit Platform in uso, il numero di test eseguiti resta zero senza alcun errore visibile. La build risulta verde perché tecnicamente nessuna asserzione fallisce, ma nessuna asserzione viene nemmeno valutata.

Come si evita il problema del falso verde su ArchUnit?

Sostituendo i campi @ArchTest con metodi @Test ordinari di JUnit Jupiter, importando le classi una volta in @BeforeAll con ClassFileImporter e chiamando esplicitamente rule.check() dentro ogni metodo di test. Questo pattern usa lo stesso motore Jupiter che esegue già tutti gli altri test del modulo, quindi viene sempre effettivamente eseguito.

Cos'è FreezingArchRule e a cosa serve?

FreezingArchRule è un wrapper di ArchUnit che salva le violazioni esistenti di una regola in uno store versionato, permettendo alla build di restare verde per il debito storico già presente, ma fallendo immediatamente se viene introdotta una violazione nuova non ancora congelata. Rende il debito tecnico tracciabile invece di doverlo risolvere tutto subito o ignorarlo del tutto.

Regole ArchUnit a scope stretto o gate globale: quale scegliere?

Non è una scelta esclusiva. Le regole a scope stretto (su una classe o package specifico) sono utili durante lo sviluppo attivo di una feature perché danno feedback immediato e senza falsi positivi. Un gate globale con FreezingArchRule copre l'intera codebase e previene nuove regressioni ovunque, incluse le aree non coperte dalle regole narrow.

Da quante classi in poi conviene introdurre ArchUnit in un progetto Spring Boot?

Non esiste una soglia universale, ma nell'esperienza diretta su questo backend il punto di rottura è arrivato tra le 500 e le 1.000 classi Java, quando ricordare a memoria quali layer dipendono da quali è diventato impossibile e le violazioni hanno iniziato a essere introdotte per distrazione più che per scelta.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti