Debito TecnicoArchitetturaSoftware Delivery

Come ridurre il debito tecnico: strategie che funzionano

Ridurre il debito tecnico non significa riscrivere tutto da zero né fermare la delivery per mesi. Significa intervenire nei punti giusti, nell'ordine giusto, mantenendo attiva la capacità di rilasciare valore durante tutto il processo.

QMates· Software Advisory14 aprile 202611 min

Il big bang rewrite: una scelta ad alto rischio, non una soluzione

Quando il debito tecnico è diventato abbastanza visibile da entrare nell'agenda del management, la proposta che emerge più spesso è anche la più rischiosa: riscrivere tutto da zero. Il ragionamento è intuitivo — il sistema attuale è il problema, un sistema nuovo risolverebbe il problema — ma ignora alcune realtà operative che rendono questo approccio statisticamente rischioso: nasconde costi significativi, richiede di mantenere il sistema vecchio funzionante in parallelo, e tende a replicare gli stessi problemi del vecchio sistema nel nuovo se le cause strutturali non vengono affrontate.

La prima è che il sistema attuale, per quanto problematico, incorpora anni di conoscenza del dominio: edge case che nessuno ricorda di aver gestito, comportamenti che il business considera impliciti, integrazioni documentate solo nel codice. Una riscrittura parte da zero anche da questa conoscenza, e tende a scoprire la complessità nascosta nel momento peggiore — quando il sistema nuovo deve andare in produzione.

La seconda è il costo del parallelismo. Durante una riscrittura completa, il team deve mantenere il sistema vecchio in produzione mentre sviluppa il nuovo. Il debito tecnico continua a limitare la velocità sul vecchio sistema; il nuovo sistema richiede risorse proporzionali alla sua ambizione. Il risultato tipico è che entrambi i sistemi avanzano più lentamente del previsto, e la finestra temporale prima della transizione si allunga oltre ogni stima iniziale.

La terza è il rischio del deployment. Un sistema nuovo che deve sostituire completamente un sistema vecchio richiede un momento di transizione che è, per definizione, il momento di massima esposizione al rischio. Non esiste un modo di mitigarlo completamente; esiste solo un modo di rimandarlo fino al punto in cui non è più rimandabile.

Esistono scenari in cui la riscrittura completa è la scelta corretta — ma sono identificabili con criteri precisi, non con la frustrazione accumulata. I trigger specifici (tecnologie non più manutenute, architettura talmente accoppiata da rendere impossibile qualsiasi isolamento, discontinuità strategica) sono trattati in dettaglio nel prossimo articolo di questa serie: legacy software — quando convivere, quando evolvere e quando riscrivere davvero.

Il principio che guida gli interventi efficaci

La strategia che funziona per ridurre il debito tecnico è sempre la stessa, indipendentemente dalla tecnologia e dal contesto: intervenire in modo incrementale, mantenendo attiva la capacità di rilasciare valore durante tutto il processo.

Questo principio ha tre implicazioni operative. La prima è che ogni intervento deve essere abbastanza piccolo da poter essere completato e rilasciato in un tempo ragionevole — di solito uno o due sprint — senza che la delivery di nuove feature si blocchi. La seconda è che l'ordine degli interventi deve essere determinato dall'impatto sulla delivery, non dalla "pulizia" del codice. La terza è che ogni intervento deve lasciare il sistema in uno stato migliore del precedente, non solo in uno stato intermedio verso un obiettivo lontano.

Questo ultimo punto è critico. Il debito tecnico si accumula precisamente nei momenti in cui si lavora in stati intermedi: si inizia un refactoring, ci si ferma a metà per una urgenza, lo stato intermedio diventa il nuovo stato permanente. Ogni intervento di riduzione del debito deve essere completo in sé: se non puoi finirlo, non iniziarlo.

Strangler fig: la strategia per i sistemi legacy

Il pattern più efficace per sostituire un sistema legacy senza un big bang rewrite è lo Strangler Fig, proposto da Martin Fowler. Il nome è preso dal fico strangolatore: una pianta che cresce attorno a un albero ospite, lo avvolge progressivamente, e lo sostituisce nel tempo senza che ci sia mai un momento di transizione brusca.

In pratica, si costruisce il nuovo sistema attorno al vecchio. Ogni nuova funzionalità viene implementata nel nuovo sistema; le funzionalità esistenti vengono migrate una alla volta, dalla più semplice alla più complessa, man mano che il nuovo sistema matura. Il vecchio sistema continua a funzionare per le parti non ancora migrate; il nuovo sistema cresce progressivamente fino a sostituirlo completamente.

Il vantaggio di questo approccio è che il rischio è distribuito nel tempo: ogni migrazione è un passo piccolo e reversibile. Il nuovo sistema può essere testato in produzione prima di sostituire completamente il vecchio. La transizione non ha un giorno X in cui tutto cambia; avviene gradualmente, con un punto di non ritorno che si raggiunge quando la percentuale di traffico sul nuovo sistema è abbastanza alta da rendere il vecchio obsoleto.

Lo svantaggio è il costo del parallelismo nel periodo di transizione: due sistemi da mantenere invece di uno. Per questo la durata del periodo di coesistenza deve essere limitata e pianificata dall'inizio.

Refactoring progressivo: dove e come intervenire

Per il debito che non richiede una riscrittura completa — che è la maggior parte del debito reale — il refactoring progressivo è l'approccio più efficace. Ma il termine "refactoring" è spesso usato in modo troppo generico: qualsiasi miglioramento del codice viene chiamato refactoring, indipendentemente dal fatto che abbia un impatto misurabile sulla delivery.

Il refactoring che conta non è quello che rende il codice più bello; è quello che riduce il costo delle modifiche future in aree ad alta frequenza di cambiamento. La distinzione è operativa. Prima di iniziare qualsiasi intervento di refactoring, la domanda è: questa modifica, una volta completata, ridurrà il cycle time delle modifiche future in questa area? Se la risposta non è chiara, l'intervento probabilmente non è nella lista delle priorità giuste.

Il punto di partenza del refactoring progressivo è sempre la mappa del debito costruita con le metriche descritte nell'articolo precedente: quali aree del sistema hanno il cycle time più alto? Dove il bug rate è più elevato? Dove il cost of change è sproporzionato? Queste aree sono quelle in cui l'investimento nel refactoring ha il ritorno più alto. Fare refactoring nelle aree sbagliate è uno degli sprechi più comuni: migliora la leggibilità del codice ma non sposta la velocità di delivery.

La sequenza che funziona è bottom-up per complessità, top-down per impatto. Si inizia dagli interventi con il rapporto impatto/sforzo più favorevole: spesso non sono le ristrutturazioni architetturali grandi, ma le estrazioni di logica duplicata, i chiarimenti di confini di modulo, le rimozioni di dipendenze circolari nelle aree ad alta frequenza di modifica. Questi interventi sono completabili in pochi giorni, riducono immediatamente la complessità operativa e creano lo spazio per gli interventi più ampi.

Come mantenere la delivery durante l'intervento

Il rischio principale del refactoring è che rallenti la delivery durante il processo. Questo accade quasi sempre quando l'intervento è troppo ampio: si tocca troppo codice in una volta sola, si crea instabilità nel sistema, i test falliscono in modo imprevedibile, e il team dedica più tempo a stabilizzare che a sviluppare.

Le tecniche che riducono questo rischio sono consolidate. La prima è il branch by abstraction — una tecnica introdotta da Stacy Curl nel 2007 e poi diffusa da Paul Hammant —: invece di modificare direttamente il codice esistente, si introduce un'astrazione che isola la parte da riscrivere, si reimplementa dietro quella astrazione, si verifica che il comportamento sia equivalente, e solo allora si rimuove il vecchio codice. In questo modo il sistema rimane sempre in uno stato funzionale durante l'intervento.

La seconda è il feature flag: il nuovo codice esiste in produzione ma è inattivo finché non viene attivato deliberatamente. Questo permette di rilasciare il refactoring senza renderlo visibile, di testarlo su una percentuale del traffico, e di fare rollback in pochi secondi se emergono problemi.

La terza è la copertura dei test prima di iniziare. Fare refactoring senza una copertura adeguata è come smontare un motore senza sapere come dovrebbe funzionare. Prima di toccare un'area del sistema, si aggiungono i test che descrivono il comportamento attuale — anche se quel comportamento non è quello desiderato. I test esistenti diventano la rete di sicurezza che permette di procedere con confidenza.

La prioritizzazione: dove iniziare

Con un sistema che ha debito distribuito su più aree, il problema della prioritizzazione è concreto: da dove si inizia? La risposta non è "dall'area più complessa" né "dall'area con il codice più vecchio". È dall'area dove l'investimento ha il ritorno più alto sulla delivery nel breve termine.

Tre criteri da applicare in sequenza per prioritizzare gli interventi.

Il primo è la frequenza di modifica. Un'area che cambia ogni sprint e ha alto debito è più urgente di un'area che cambia raramente e ha lo stesso livello di debito. Il costo del debito si paga ogni volta che si modifica quella parte del sistema: più frequentemente si modifica, più velocemente si ammortizza l'investimento nel refactoring.

Il secondo è il peso sulle dipendenze. Un modulo da cui dipendono dieci altri moduli blocca più delivery di un modulo isolato. Se il collo di bottiglia architetturale è un singolo punto di accoppiamento che rallenta modifiche in cinque aree diverse del sistema, intervenire lì sblocca cinque flussi di lavoro contemporaneamente.

Il terzo è la reversibilità dell'intervento. Se non sei sicuro dell'approccio, inizia dall'intervento più facilmente reversibile: quello che puoi disfare in poche ore se non funziona come previsto. Gli interventi irreversibili vanno pianificati con maggiore cura e validati in ambienti controllati prima di entrare in produzione.

Cosa fare concretamente

  1. Scegli un'area, non il sistema intero: il primo intervento di riduzione del debito non deve essere il più ambizioso. Deve essere quello con il rapporto impatto/sforzo più chiaro. Prendi l'area con il cycle time più alto e il bug rate più elevato. Definisci un intervento specifico, completabile in uno o due sprint, che riduca il costo delle modifiche in quella area. Misura il cycle time dopo l'intervento. Se il trend migliora, hai la conferma che la prioritizzazione era corretta.
  2. Aggiungi test prima di toccare il codice: se l'area che vuoi refactorare non ha copertura di test sufficiente, la prima azione è aggiungere test sul comportamento attuale. Non test che validano il design; test che descrivono cosa il sistema fa. Questi test sono la rete di sicurezza che permette di modificare il codice con confidenza. Senza, ogni modifica è un rischio non quantificato.
  3. Definisci una metrica di successo prima di iniziare: ogni intervento di riduzione del debito deve avere un criterio di successo misurabile. Non "il codice è più pulito"; "il cycle time delle modifiche in questa area è sceso da X a Y giorni". Questa metrica è il modo per sapere se l'intervento ha funzionato, e per comunicare al management il ritorno dell'investimento in termini che sa leggere.

La prospettiva QMates

Il principio che guida il nostro approccio alla riduzione del debito è uno: non intervenire mai in modo che blocchi la capacity di rilasciare valore. Il debito tecnico ha già rallentato la delivery; un intervento che la blocca ulteriormente, anche se temporaneamente, aggrava il problema invece di risolverlo.

Nella pratica, questo significa che ogni intervento che proponiamo è progettato per essere eseguito in parallelo alla delivery ordinaria, non al posto di essa. Non chiediamo uno "sprint di cleanup" isolato dal resto del lavoro; proponiamo un modo di integrare il refactoring nel flusso normale di sviluppo, con criteri chiari su quando un'area è abbastanza stabile da non richiedere ulteriore attenzione.

Il nostro servizio di intervento mirato è progettato esattamente per questo: stabilizzare le aree bloccate del sistema nel minor tempo possibile, mantenendo attiva la delivery durante tutto il processo. Non è un refactoring estetico; è un intervento chirurgico su ciò che blocca il flusso di valore, con metriche di ingresso e di uscita definite prima di iniziare.

Il sesto e ultimo articolo di questa serie si occupa di un caso specifico di riduzione del debito: il sistema legacy — quando convivere, quando evolvere e quando riscrivere davvero.

Raccontaci dove sei bloccato

Prototipo fragile, legacy pesante o delivery imprevedibile – partiamo da lì