Un record di decisione senza le sue assunzioni è solo un verdetto

Apri quasi qualunque log di decisioni e troverai verdetti.

Abbiamo scelto Postgres. Approvato: migrare il servizio di billing sulla nuova piattaforma. Decisione: usare una coda gestita per le notifiche. A volte c'è una data, a volte un nome, ogni tanto un paragrafo di motivazioni scritto al passato da qualcuno che era già convinto.

Poi, un anno dopo, qualcuno fa l'unica domanda per cui esiste un record di decisione: dovremmo ancora fare così? E il record non sa rispondere. Ti dice cosa è stato deciso. Non ti dice cosa doveva essere vero perché fosse la scelta giusta, quindi oggi non c'è niente con cui confrontarlo.

Il primo post di questa serie sosteneva che le decisioni che non scrivi diventano debito. Scriverle si è rivelata la parte facile. È in ciò che scrivi che la maggior parte dei record fallisce senza far rumore, e il fallimento non si vede, perché il log sembra completo. Ogni decisione è lì. Nessuna si può riaprire.

Il verdetto è la parte meno utile

Ecco la parte controintuitiva. Il verdetto è ciò che tutti registrano, ed è ciò che serve di meno.

Il sistema mostra già cosa è stato scelto. Il database è Postgres. Il servizio di billing gira sulla nuova piattaforma. Chiunque può vedere il verdetto guardando ciò che esiste. Quello che nessuno può vedere guardando è perché esiste così, cos'altro era stato considerato, e cosa davano per scontato sul mondo le persone nella stanza quando hanno scelto.

Quella metà mancante è proprio ciò che decide se una decisione invecchia bene. Una scelta giusta in certe condizioni diventa sbagliata quando le condizioni cambiano, e se le condizioni non sono mai state scritte, nessuno si accorge del momento in cui sono cambiate.

Nemmeno la deliberazione completa è la risposta

La soluzione ovvia è registrare di più. Catturare la discussione, i verbali, il ragionamento completo, ogni argomento a favore e contro.

L'ho visto fare, e fallisce dall'altra parte. Una deliberazione completa si registra una volta e non si legge mai. Sono venti pagine che nessuno apre, e quando finalmente qualcuno lo fa, l'unica frase che conta è sepolta sotto tutto ciò che non conta.

Il processo è importante. Ma solo una fetta stretta si guadagna un posto nel record: la parte che permette a qualcuno, più avanti, di capire se la decisione poggia ancora su un terreno solido.

Sei campi che si guadagnano il posto

Dopo averne scritti un buon numero, queste sono le sezioni che tengo.

Contesto. Cosa si sapeva allora, con la data. Non tutta la storia, solo quanto basta perché un lettore tra un anno possa giudicare la decisione in modo equo, rispetto a ciò che si poteva sapere allora e non a ciò che oggi è ovvio.

Decisione. Il verdetto, in una o due frasi. È il campo meno informativo e dovrebbe essere il più corto.

Opzioni scartate. Cos'altro c'era sul tavolo e il motivo principale per cui ha perso. È il campo che impedisce di rifare lo stesso dibattito tra diciotto mesi, perché il prossimo che propone l'opzione scartata può vedere perché ha perso e se quel motivo regge ancora.

Assunzioni. Cosa deve restare vero perché questa sia la decisione giusta. Il volume resta sotto una soglia. Un provider mantiene una garanzia. Un team continua a possedere un componente. È il cuore del record, ed è il campo che la maggior parte dei record non ha.

Esito atteso. Cosa crediamo che succederà se abbiamo ragione. Un numero se possibile, altrimenti un cambiamento osservabile.

Da riaprire quando. Le condizioni specifiche e osservabili che dovrebbero riportare qualcuno a questa decisione. Non una data in calendario, che è un rituale che qualcuno deve ricordarsi di celebrare, ma un segnale: una metrica che supera una soglia, il rinnovo di un contratto, una dipendenza che cambia proprietario.

Esito atteso e assunzioni fanno lavori diversi

Un suggerimento che sento spesso è di registrare l'esito atteso e più avanti confrontarlo con ciò che è successo davvero. È così che scopri se la decisione era giusta.

È un buon campo, e lo tengo. Ma da solo giudica la cosa sbagliata.

Un esito mescola la qualità di una decisione con il caso. Una buona decisione può andare male e una cattiva può funzionare, e confrontare l'atteso con l'osservato non ti dice quale delle due cose sia successa. I giocatori di poker hanno un nome per l'abitudine di giudicare le decisioni dai risultati, e non è un complimento.

Ciò che separa le due cose sono le assunzioni. Se hanno retto e l'esito è stato comunque cattivo, era varianza, e la decisione poteva essere giusta. Se si sono rotte, la decisione andava riaperta, e la domanda è perché nessuno se ne sia accorto.

C'è anche una differenza di tempi che in pratica pesa ancora di più. L'esito di solito arriva tardi, mesi o anni dopo la decisione, quando cambiare rotta costa caro. Le assunzioni si possono verificare finché la decisione è ancora viva. Quindi i due campi fanno lavori diversi. L'esito atteso giudica la decisione a posteriori. L'assunzione ti dice quando riaprirla in tempo.

Il template

Questa è la forma che uso, con un esempio compilato. I numeri sono illustrativi; il punto è la struttura.

# 0007. Use a task queue for outbound notifications

Status: accepted · 2026-03-12 · Owner: platform team

## Context

- Notifications are sent by a scheduled job.
- Production shows duplicates.

## Decision

Move outbound notifications to a managed task queue.

## Options rejected

- Keep the scheduler and add a lock: fixes duplicates, not retries.

## Assumptions

- Volume stays under 50k messages per day.
- The provider's dedup window (1 hour) is longer than our worst retry gap.

## Expected outcome

- Zero duplicates in a month of production traffic.

## Reopen when

- Daily volume exceeds 40k for a week.
- A retry gap longer than 45 minutes is observed.

Due dettagli sono voluti. Le assunzioni e le condizioni di riapertura si rispecchiano: ogni condizione di riapertura è un'assunzione che sta diventando falsa, intercettata un po' prima che si rompa. E le condizioni di riapertura sono segnali osservabili, non date di revisione, perché nell'anniversario di una decisione non succede niente.

Fai rispettare il record da solo

Un template è un suggerimento, e i suggerimenti si erodono. Il secondo post di questa serie sosteneva che un confine che non fai rispettare è solo un desiderio, e i record di decisione non fanno eccezione. Affidati alle buone intenzioni, i primi record sono completi, e al decimo la sezione delle assunzioni dice "TBD".

Quindi il controllo vive in CI. Rifiuta qualunque record a cui manchi una delle sei sezioni, o le cui assunzioni e condizioni di riapertura non contengano voci reali.

// scripts/check-adr.mjs
// Fails CI if a decision record is missing the sections that make it checkable later.
// Usage: node scripts/check-adr.mjs [dir]   (default: docs/adr)
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

const dir = process.argv[2] ?? 'docs/adr'
const REQUIRED = [
  'Context',
  'Decision',
  'Options rejected',
  'Assumptions',
  'Expected outcome',
  'Reopen when',
]
// Sections that must contain at least one real bullet, not a placeholder.
const MUST_HAVE_BULLETS = ['Assumptions', 'Reopen when']

const sections = text => {
  const out = new Map()
  let current = null
  for (const line of text.split('\n')) {
    const heading = line.match(/^##\s+(.+?)\s*$/)
    if (heading) {
      current = heading[1]
      out.set(current, [])
    } else if (current) {
      out.get(current).push(line)
    }
  }
  return out
}

const isPlaceholder = s => /^(-|\*)\s*(tbd|todo|n\/a|none|\.\.\.)?\s*$/i.test(s.trim())

const problems = []
const files = readdirSync(dir).filter(f => /^\d{4}-.+\.md$/.test(f))

for (const file of files) {
  const parsed = sections(readFileSync(join(dir, file), 'utf8'))
  for (const name of REQUIRED) {
    if (!parsed.has(name)) problems.push(`${file}: missing "## ${name}"`)
  }
  for (const name of MUST_HAVE_BULLETS) {
    const bullets = (parsed.get(name) ?? []).filter(
      l => /^\s*[-*]\s+/.test(l) && !isPlaceholder(l)
    )
    if (parsed.has(name) && bullets.length === 0) {
      problems.push(`${file}: "## ${name}" has no real entries`)
    }
  }
}

if (files.length === 0) {
  console.error(`No decision records found in ${dir}`)
  process.exit(1)
}

if (problems.length > 0) {
  console.error(`Decision records incomplete:\n  ${problems.join('\n  ')}`)
  process.exit(1)
}

console.log(`OK: ${files.length} decision record(s) state their assumptions and reopen conditions`)

È volutamente stupido. Non giudica se un'assunzione è buona, solo che qualcuno ha dovuto scriverne una. Basta per cambiare l'abitudine, perché il modo più economico di far passare il controllo è pensare per due minuti a ciò su cui la decisione poggia, e quei due minuti sono tutto il punto.

C'è un tetto

Non tutte le decisioni lo meritano. La maggior parte si inverte a basso costo, e per quelle basta una riga nella descrizione della pull request. Il record vale il suo costo dove sbagliare è costoso e plausibile: una forma su cui altri team costruiranno, un impegno fuori dall'azienda, dati che non puoi ricreare. Registrare ogni scelta con sei sezioni è di nuovo la deliberazione da venti pagine, in un altro formato.

La regola che uso è la stessa che la serie usa per la reversibilità. Paga il record dove la decisione sarà difficile da invertire, e saltalo ovunque altro.

La regola che ti lascio

Il verdetto ti dice cosa è stato deciso. Le assunzioni ti dicono se è ancora giusto. Un record che ha il primo senza le seconde è un verdetto con una data sopra, e sembrerà completo fino al momento esatto in cui ne avrai bisogno.

Quindi prendi l'ultima decisione che il tuo team ha registrato e leggila come la leggerebbe un estraneo. Riusciresti a capire, dal solo record, se è ancora vera? E se domani smettesse di esserlo, cosa nel record direbbe a qualcuno di andare a guardare?