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.
| 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 |
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 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.
Commenti
Caricamento commenti...
Accedi per commentare