Congelare il debito invece di negarlo: 225 violazioni in un file, e la riga che le azzera
Hai scritto il gate architetturale, lo hai lanciato, e la suite è diventata rossa in 225 punti. Nessuno di quei 225 l'hai scritto tu, nessuno si ripara in un pomeriggio, e intanto devi consegnare. A questo punto ci sono tre strade, e due sono sbagliate: disattivare il gate, oppure aprire un cantiere di bonifica che nessuno finirà.
La terza strada è dichiarare quelle violazioni come stato di fatto, scriverle in un file, e far fallire il gate soltanto sulla duecentoventiseiesima. Il debito resta, ma smette di crescere, e la suite torna verde senza mentire. In ArchUnit questo si chiama FreezingArchRule, e in questo progetto è attivo su due regole globali.
Questo articolo mostra che cosa fa davvero il congelamento, misurato in quattro esperimenti eseguiti su questo codice: la prima corsa, le corse successive, l'arrivo di una violazione nuova, e la risoluzione di una vecchia. Poi mostra la riga che può azzerare l'intera base di riferimento senza che nessuno se ne accorga.
Che cosa c'è nel file congelato
La base di riferimento di questo progetto vive in backend/src/test/resources/archunit-store e contiene tre file: un indice chiamato stored.rules e due file con nomi casuali, uno per regola. L'indice è un file di proprietà Java che associa la descrizione della regola al nome del file che ne contiene le violazioni.
#Tue Jul 07 15:26:19 CEST 2026
domain\ puro\:\ niente\ Spring/JPA\ (4\ violazioni\ storiche\ congelate,\ audit\ 2026-07-07)=843617c9-...
presentation\ →\ application\ →\ infrastructure\:\ i\ controller\ non\ importano...=be0c35c3-...
Le due righe sono la chiave di tutto ciò che segue, e vanno guardate bene: a sinistra dell'uguale c'è il testo della descrizione della regola, con gli spazi e i due punti preceduti dalla barra rovesciata come vuole il formato delle proprietà. Non c'è nessun identificativo stabile, nessun nome di metodo, nessun numero di versione. L'unica cosa che collega una regola alle sue violazioni congelate è la sua descrizione in forma di testo.
Il file delle violazioni della regola sulla presentazione contiene 225 righe. Le ho classificate per tipo di dipendenza, perché il tipo dice quanto costa ripararle:
| Tipo di dipendenza | Righe | Che cosa significa |
|---|---|---|
| Chiamata a metodo | 146 | Il controller invoca un metodo di una classe di infrastruttura |
| Parametro di costruttore o metodo | 26 | L'adattatore è iniettato nel controller |
| Campo o tipo dichiarato | 26 | Il controller tiene un riferimento tipizzato |
| Annotazione | 17 | Il controller usa un'annotazione definita nell'infrastruttura |
| Altro | 10 | Forme residue di dipendenza |
Le 225 righe si concentrano in 29 classi distinte, quasi tutte controller. Il rapporto fra i due numeri è la cosa interessante: in media quasi otto violazioni per classe, e il grosso sono chiamate a metodo. Vuol dire che il difetto non è «un controller ha una dipendenza di troppo» ma «un controller usa l'infrastruttura come se fosse il suo strato applicativo», e che la riparazione di una classe elimina otto righe insieme.
Esperimento uno: che cosa cambia il congelamento
Ho preso la regola del dominio puro, l'ho eseguita nelle due forme, e ho mandato le violazioni in una base di riferimento vuota creata per l'occasione. Il file di configurazione che decide dove sta la base è questo, e va sul classpath di test con il nome archunit.properties:
freeze.store.default.path=/percorso/della/base
freeze.store.default.allowStoreCreation=true
freeze.store.default.allowStoreUpdate=true
Le tre proprietà decidono tre cose diverse e vanno capite separatamente. path è il percorso della cartella, e se non la dichiari ArchUnit cerca archunit_store nella cartella di lavoro, cioè un posto che cambia a seconda di come lanci la build. allowStoreCreation vale false se non lo scrivi, e con quel valore la prima corsa fallisce perché la base non esiste: va messo a true solo quando crei la base la prima volta, e in molti progetti viene poi riportato a false perché una base creata per sbaglio su una macchina qualsiasi è una base che congela qualunque cosa ci fosse in quel momento. allowStoreUpdate vale true per impostazione predefinita, e governa il restringimento di cui parliamo fra poco.
1. regola NON congelata -> FALLISCE, violazioni = 7
2. regola congelata, prima corsa -> PASSA
3. regola congelata, seconda corsa -> PASSA
La prima corsa congelata passa, e questo è il punto che genera più diffidenza: sembra che lo strumento stia nascondendo il problema. Non lo nasconde, lo scrive. Dopo la corsa due la cartella contiene un file con le sette righe di violazione, leggibile, versionabile e diffabile come qualsiasi altro file di progetto. La differenza con la disattivazione del gate è che il debito è adesso un artefatto del repository, con una data, che compare nelle revisioni del codice quando cambia.
Esperimento due: la violazione nuova deve fallire
Il congelamento vale qualcosa solo se la violazione successiva fa fallire la build. Per verificarlo ho congelato una base ristretta, con le sole cinque violazioni delle porte di dominio, e poi ho eseguito la stessa descrizione di regola su tutto il dominio, dove le violazioni sono sette.
1. congelo le sole porte (5 violazioni) -> PASSA
2. stessa descrizione, ora su tutto il dominio -> FALLISCE, violazioni nuove = 2
Class <...domain.bloodwork.BloodworkInterpreter> is annotated with <org.springframework.stereotype.Component>
Class <...domain.termsheet.TermSheetAnalyzer> is annotated with <org.springframework.stereotype.Component>
3. terza corsa su tutto il dominio -> FALLISCE, violazioni nuove = 2
Il messaggio di fallimento contiene due righe, non sette: le cinque già congelate restano invisibili e il gate parla solo delle novità. È esattamente il comportamento che serve a chi legge una build rossa, perché l'elenco contiene solo cose che riguardano il lavoro appena fatto.
La terza corsa è l'esperimento che conta davvero. Se il congelamento assorbisse da sé le violazioni nuove, la seconda esecuzione tornerebbe verde e il gate sarebbe un contatore inutile. Non lo fa: continua a fallire sulle stesse due righe fino a quando qualcuno le ripara o le aggiunge deliberatamente alla base. Questa è la proprietà che distingue il congelamento da una soglia numerica, ed è il motivo per cui preferisco congelare piuttosto che scrivere «non più di 225 violazioni».
Esperimento tre: la base si stringe da sé
Il rischio di una base di riferimento è che diventi un cimitero: le violazioni risolte restano dentro, il file non si aggiorna mai, e dopo un anno nessuno sa più quali di quelle righe descrivano codice ancora esistente. ArchUnit risolve la cosa togliendo da sé le righe che non trova più.
1. congelo tutto il dominio -> PASSA
righe nello store = 7
2. due violazioni risolte (regola sulle sole porte) -> PASSA
righe nello store = 5
Nella seconda corsa ho ristretto la regola alle sole porte, che è il modo più rapido di simulare «le due classi annotate sono state ripulite». La base passa da sette righe a cinque, e la corsa resta verde. Il comportamento si chiama restringimento automatico, dipende da allowStoreUpdate, ed è il motivo per cui il file nella cartella cambia anche quando non hai toccato il test.
La conseguenza operativa va detta chiaramente, perché genera confusione nelle revisioni del codice: un cambiamento nei file della base di riferimento è un esito atteso, non un errore da annullare. Nel progetto lo dichiara un commento nel file di configurazione, e serve esattamente a impedire che qualcuno ripristini il file pensando di aver sporcato il repository per sbaglio.
La riga che azzera tutto, in silenzio
Adesso la trappola. L'indice associa la descrizione della regola al suo file di violazioni, e la descrizione è un testo libero che chiunque può migliorare. Ho eseguito la regola congelata quattro volte, cambiando solo il testo di as(...) all'ultima.
static ArchRule regola(String testo) {
return noClasses()
.that().resideInAPackage("dev.federicocalo.portfolio.domain..")
.should().dependOnClassesThat()
.resideInAnyPackage("org.springframework..", "jakarta.persistence..")
.as(testo);
}
2. regola congelata, prima corsa -> PASSA
3. regola congelata, seconda corsa -> PASSA
4. stessa regola, testo di as() cambiato -> PASSA
Il punto quattro passa, e la cartella adesso contiene due file di violazioni e due righe nell'indice: una per «domain puro» e una per «domain puro (riscritto)». ArchUnit non ha riconosciuto la regola, ha creato una base nuova, e l'ha riempita con tutto quello che ha trovato. Qualunque violazione esistesse in quel momento è diventata parte della normalità, senza nessun avviso.
Tradotto in una giornata di lavoro vera: qualcuno riformula il messaggio della regola perché era scritto male, la build resta verde, e la base di riferimento si è azzerata. Se nel frattempo erano entrate dieci violazioni nuove, adesso sono congelate. Il gate continua a esistere e a non proteggere niente di ciò che è successo prima.
- Il testo di
as(...)è una chiave, non un commento: si tratta come il nome di una colonna di database. - Metti un avviso nel codice, sopra la regola, che dica che cambiare quella stringa resetta la base.
- Guarda l'indice nelle revisioni: una riga nuova in
stored.rulessignifica una regola nuova oppure una base azzerata. - Riporta
allowStoreCreationafalsedopo la prima creazione: così una chiave sconosciuta fallisce invece di creare una base nuova.
L'ultimo punto è la difesa strutturale, e in questo progetto non è attiva: la proprietà è a true con un commento che spiega perché. Le altre tre sono difese di processo, e nel sorgente del test ci sono infatti due commenti in maiuscolo, uno sopra ciascuna regola congelata, che dicono di non toccare il testo della descrizione. Un commento non è un vincolo, ma è l'unica cosa che vede chi sta per cambiare quella riga.
Chiusura
Adesso sai introdurre un gate architetturale su un progetto che ha già debito, senza disattivarlo e senza aprire una bonifica: congeli lo stato di fatto, la suite torna verde, e il file delle violazioni diventa un artefatto versionato con una data.
Sai anche che cosa il congelamento garantisce e che cosa no, perché l'hai visto misurato: una violazione nuova fa fallire la build e continua a farla fallire, una violazione risolta esce da sé dalla base, e la base non assorbe niente da sola. Tre comportamenti che si verificano in due minuti sul tuo progetto con lo stesso programma.
E sai qual è l'unica cosa che può annullare tutto: il testo della descrizione della regola, che è la chiave dell'indice, e che chiunque può riscrivere pensando di migliorare un messaggio. Il prossimo articolo della serie lascia l'architettura delle classi e passa a quella dei dati, con 219 migrazioni Flyway, quattro buchi nella numerazione e un'opzione che serve a non far fallire il deploy.
Commenti
Caricamento commenti...
Accedi per commentare