Il debito tecnico diventa una decisione di business quando ha un tempo di rientro
C'è un consiglio di capacity planning che ormai è quasi consenso, ed è un buon consiglio. Misura il lavoro non pianificato per un trimestre. Probabilmente si mangia tra un quarto e un terzo del team. Smetti di fingere che non ci sia, riserva quella quota come margine e pianifica la roadmap su ciò che resta.
Funziona. La roadmap smette di slittare ogni mese, e il team smette di scusarsi per incidenti che non avrebbe mai potuto pianificare.
Ha però un effetto collaterale di cui nessuno parla. Una volta che il margine è una riga del piano, il lavoro non pianificato ha una casa. Non compete più con niente, non sorprende più nessuno, e può restare esattamente della dimensione che ha, per sempre. Misurato solo per quantità, il margine è un budget. E un budget, una volta approvato, è l'ultima cosa che qualcuno va a guardare.
Il secchio non è casuale
Guarda dentro il lavoro non pianificato di un qualunque team che gestisce un sistema da qualche anno e raramente troverai una distribuzione casuale di sfortuna. Troverai sempre gli stessi pochi mittenti.
Un'integrazione che nessuno possiede e che si rompe ogni volta che l'altra parte fa un deploy. Un batch che va riavviato a mano dopo ogni chiusura di fine mese. Un certificato che qualcuno rinnova a mano. Un database di reportistica condiviso che quattro team leggono direttamente, così ogni modifica allo schema produce una settimana di ticket "urgenti". Una dipendenza indietro di due major version, che trasforma ogni bollettino di sicurezza in un piccolo progetto.
Ognuno di questi è stato una decisione, di solito ragionevole: rilasciare l'integrazione adesso e irrobustirla dopo, condividere il database perché era più veloce che costruire un'API. Il primo post di questa serie sosteneva che le decisioni che non scrivi diventano debito. È qui che il debito manda il conto. Gli interessi non si pagano su un conto separato. Si pagano in ore, dal margine, da chiunque sia di turno quel giorno.
Perché il debito perde ogni riunione di pianificazione
Il debito tecnico ha un problema di reputazione, e a causarlo siamo soprattutto noi tecnici. Chiediamo di ripagarlo nella nostra lingua. Il codice è disordinato. Il design è sbagliato. Ci farà male più avanti.
Dall'altra parte del tavolo, una feature arriva con una stima di ricavi, il nome di un cliente, una data. Quando le due cose si incontrano in pianificazione vince la feature, ogni volta, ed è giusto così. Da una parte c'è un numero, dall'altra una sensazione.
La soluzione non è insistere di più. È dare al debito un numero nella stessa valuta della feature. Gran parte di quel numero ce l'hai già, perché gli interessi li stai pagando adesso, nel lavoro non pianificato. Solo che nessuno scrive dove vanno.
Tagga la sorgente, non la categoria
Il cambiamento è piccolo. Ogni attività non pianificata riceve un campo in più: la sorgente da cui arriva.
Poche regole fanno la differenza tra un campo che ti dice qualcosa e un campo che si riempie di rumore.
La sorgente nomina qualcosa che potresti correggere. "Incidente", "bug" e "supporto" sono categorie. Dicono che tipo di lavoro era, non da dove arrivava. Una sorgente è un sistema, un'integrazione, un job, una decisione: partner-sync, manual-certs, shared-reporting-db. Se non riesci a immaginare un progetto che faccia sparire la sorgente, non è ancora una sorgente.
Tagga chi ha fatto il lavoro. Non l'owner del sistema da cui è arrivato, che ha tutto l'interesse a vedere il problema altrove.
"Senza tag" è ammesso, e si conta. Costringere a indovinare produce finzione. Lasciare il campo vuoto e contare i vuoti ti dice quanta capacità va in un posto che non sai nominare, e questo è già un risultato.
La lista resta corta. Se in un trimestre escono sessanta sorgenti diverse, accorpale. Ti servono le dieci o quindici che contano, con nomi abbastanza coerenti perché le ore si sommino.
La maggior parte dei sistemi di ticketing lo permette con un'etichetta o un campo personalizzato. Costa a ognuno pochi secondi per ticket. I dati che ottieni valgono più di qualunque inventario del debito scritto in un workshop, perché sono misurati sul lavoro che è successo davvero, e nessuno ha dovuto ricordarsi niente.
Trasformalo in un estratto conto degli interessi
A fine trimestre esporta i ticket e somma le ore per sorgente. Poi, per le prime della lista, stima un numero in più: grosso modo quante ore costerebbe eliminare quella sorgente. Riscrivere l'integrazione, automatizzare il rinnovo, mettere un'API davanti al database condiviso.
Dividi il costo della correzione per le ore che fa risparmiare ogni trimestre e hai un tempo di rientro.
Questo script fa il conto a partire da un export CSV, senza dipendenze:
// scripts/interest-statement.mjs
// Ranks the sources of unplanned work by the hours they cost, and the payback of fixing each.
// Usage: node scripts/interest-statement.mjs tickets.csv [fixes.csv] [quarters=1]
// tickets.csv: an export with at least the columns hours, source, planned
// fixes.csv: source, fix_hours (your estimate of what removing the source would cost)
import { readFileSync } from 'node:fs'
const [ticketsPath, fixesPath, quarters = '1'] = process.argv.slice(2)
// Minimal CSV reader: handles quoted fields and commas inside them, which ticket titles always have.
function readCsv(path) {
const rows = []
let row = [], field = '', quoted = false
const text = readFileSync(path, 'utf8')
for (let i = 0; i < text.length; i++) {
const c = text[i]
if (quoted) {
if (c === '"' && text[i + 1] === '"') { field += '"'; i++ }
else if (c === '"') quoted = false
else field += c
} else if (c === '"') quoted = true
else if (c === ',') { row.push(field); field = '' }
else if (c === '\n') { row.push(field); rows.push(row); row = []; field = '' }
else if (c !== '\r') field += c
}
if (field || row.length) { row.push(field); rows.push(row) }
const [header, ...data] = rows
const keys = header.map(h => h.trim().toLowerCase())
return data.map(r => Object.fromEntries(keys.map((k, i) => [k, (r[i] ?? '').trim()])))
}
const unplanned = readCsv(ticketsPath).filter(t => t.planned.toLowerCase() !== 'yes')
const fixes = new Map(
(fixesPath ? readCsv(fixesPath) : []).map(f => [f.source, Number(f.fix_hours)])
)
const bySource = new Map()
let total = 0
for (const t of unplanned) {
const source = t.source || 'untagged'
const hours = Number(t.hours) || 0
bySource.set(source, (bySource.get(source) ?? 0) + hours)
total += hours
}
const rows = [...bySource]
.map(([source, hours]) => {
const perQuarter = hours / Number(quarters)
const fix = fixes.get(source)
return {
source,
'hours/quarter': Math.round(perQuarter),
share: `${Math.round((hours / total) * 100)}%`,
'fix (hours)': fix ?? '',
'payback (quarters)': fix ? (fix / perQuarter).toFixed(1) : '',
}
})
.sort((a, b) => b['hours/quarter'] - a['hours/quarter'])
console.table(rows)
L'output è la pagina da portare in pianificazione. Con numeri illustrativi, per un team di sei persone su un trimestre, ha più o meno questo aspetto:
| Sorgente | Ore/trimestre | Quota | Correzione (ore) | Rientro (trimestri) |
|---|---|---|---|---|
| partner-sync | 210 | 34% | 320 | 1,5 |
| shared-reporting-db | 120 | 19% | 480 | 4,0 |
| senza tag | 95 | 15% | ||
| month-end-batch | 70 | 11% | 90 | 1,3 |
| manual-certs | 30 | 5% | 40 | 1,3 |
| tutto il resto | 100 | 16% |
Leggila come la leggerebbe la finanza. Due sorgenti si prendono più di metà del margine. Una costa 320 ore da eliminare e ne restituisce 210 ogni trimestre da lì in poi: quella correzione si ripaga in un trimestre e mezzo e continua a rendere dopo. L'altra è reale ma più lenta, e potrebbe ragionevolmente aspettare un anno. Il batch di fine mese pesa poco, ma correggerlo non costa quasi niente.
Niente di tutto questo è più una discussione sulla qualità del codice. È un investimento con un costo, un ritorno e un tempo di rientro, che compete con le feature alle stesse condizioni. A volte deve perdere. La differenza è che adesso qualcuno decide.
Poi il margine può scendere
È il pezzo che mancava al consiglio sulla capacità. Riservare un margine rende il piano onesto. Risalire alle sorgenti del margine è ciò che permette di ridurlo.
Quando il partner sync viene sistemato, l'estratto conto del trimestre dopo lo mostra. Le 210 ore non si disperdono nell'indaffaramento generale: tornano come capacità da mettere in roadmap, e puoi dimostrare da dove vengono. È anche l'unico argomento per ripagare il debito che sopravvive a un cambio di manager: le ore sono state misurate prima e dopo, dalle stesse persone, nello stesso modo.
E si ricollega ai record di decisione di cui questa serie ha già parlato. Una sorgente le cui ore salgono trimestre dopo trimestre è un'assunzione che fallisce in pubblico. Se la decisione originale aveva una condizione di riapertura, questo è il segnale che stava aspettando.
C'è un limite
Le ore sottostimano gli interessi. Non catturano il cambio di contesto, l'attività di roadmap slittata perché l'unica persona che conosceva il partner sync era impegnata a ripararlo, o il rischio della vulnerabilità che nessuno ha ancora sfruttato. Questo rende prudente la stima del rientro, ed è la direzione giusta per una stima che dovrai difendere.
Una parte del lavoro non pianificato non ha una sorgente eliminabile. Un regolatore cambia una regola, un cliente ha un'emergenza vera, il mercato si muove. Quel lavoro appartiene al margine, e l'estratto conto lo mostrerà come una lunga coda di sorgenti piccole e non ripetute. Va bene così. Lo scopo è trovare i recidivi, non portare il margine a zero.
I tag sono imprecisi. Due persone taggheranno lo stesso incidente in modo diverso, e alcune ore saranno stimate a occhio. Per ordinare dieci sorgenti per costo basta. Non usarlo per niente che richieda precisione all'ora, e non usarlo per fare classifiche delle persone.
E le stime di correzione restano stime. Sbaglieranno nella direzione solita. Una sorgente che rientra in un trimestre e mezzo regge una correzione che costa il doppio del previsto. Una che rientra in quattro trimestri probabilmente no.
La regola che ti lascio
Riserva il margine, perché fingere che il lavoro non pianificato non esista è il modo in cui falliscono le roadmap. Ma non lasciare che diventi un budget che nessuno legge. Ogni ora lì dentro arriva da qualche parte, e la maggior parte arriva dagli stessi pochi posti.
Il debito tecnico resta una lamentela tecnica finché non ha un numero. Dagli una sorgente, un costo a trimestre e un tempo di rientro, e diventa una decisione di business, l'unico tipo di decisione che venga mai presa.
Allora guarda il lavoro non pianificato dello scorso trimestre. Sapresti nominare le tre sorgenti che ti sono costate di più? E se sì, quanto costerebbe farne sparire una?