Due applicazioni possono apparire identiche dall'esterno e comportarsi in modo opposto sotto carico o dopo due anni di modifiche. La differenza non sta nel linguaggio o nel framework: sta in alcune decisioni di struttura che si prendono presto e si pagano tardi.

Race condition: il problema non è il controllo#

prenota.jsJavaScript
// La race condition più comune del web, e la più costosa: il controllo// e l'azione sono due operazioni separate, e in mezzo può succedere tutto.async function prenota(eventoId, utenteId) {  const e = await db.get('SELECT posti FROM eventi WHERE id = ?', [eventoId]);  if (e.posti < 1) throw new Error('esaurito');        // ← controllo  await db.run('UPDATE eventi SET posti = posti - 1 WHERE id = ?', [eventoId]);  await db.run('INSERT INTO prenotazioni ...');        // ← azione}//   t0  richiesta A: legge posti = 1  → passa il controllo//   t1  richiesta B: legge posti = 1  → passa il controllo//   t2  richiesta A: posti = 0//   t3  richiesta B: posti = -1       ← due biglietti per un posto solo//// Il difetto è strutturale: fra il SELECT e l'UPDATE c'è un await, e// durante un await arrivano altre richieste. Nessun "controlla meglio"// risolve il problema, perché il problema non è il controllo.

Questo schema — leggere, verificare, agire — è talmente naturale che si scrive senza pensarci. Ed è sbagliato ogni volta che fra la verifica e l'azione c'è un punto di sospensione, cioè sempre, in qualunque codice asincrono.

L'errore da evitare, quando lo si scopre, è cercare di rafforzare il controllo: rileggere il valore, controllare due volte, aggiungere un ritardo. Non funziona, perché il controllo non è il problema. Il problema è la distanza fra il controllo e l'azione.

prenota-corretto.jsJavaScript
// ✓ Soluzione 1 — il controllo dentro la scrittura. Il database valuta//   la condizione e applica la modifica come UNA operazione indivisibile.const r = await db.run(  'UPDATE eventi SET posti = posti - 1 WHERE id = ? AND posti > 0',  [eventoId]);if (r.changes === 0) throw new Error('esaurito');   // ← ha perso la corsa// ✓ Soluzione 2 — il vincolo nello schema. La rete di sicurezza che//   regge anche quando qualcuno scriverà una query dimenticandosi//   della condizione: il database rifiuta e basta.ALTER TABLE eventi ADD CONSTRAINT posti_non_negativi CHECK (posti >= 0);// ✓ Soluzione 3 — quando servono più righe insieme: transazione con//   blocco esplicito della riga.BEGIN;  SELECT posti FROM eventi WHERE id = ? FOR UPDATE;   -- blocca la riga  UPDATE eventi SET posti = posti - 1 WHERE id = ?;  INSERT INTO prenotazioni ...;COMMIT;

La prima soluzione è quella da preferire: si chiede al database di fare le due cose insieme. UPDATE ... WHERE posti > 0 valuta la condizione e applica la modifica come operazione indivisibile, e il numero di righe modificate dice chi ha vinto la corsa.

La stessa famiglia, lato browser#

invia.jsJavaScript
// La stessa famiglia, lato browser: il doppio clic sul pulsante "Invia".// ✗ Due richieste identiche, due ordini creati.bottone.addEventListener('click', async () => {  await fetch('/api/ordini', { method: 'POST', body: ... });});// ✓ Disabilitare non basta (il clic può partire prima del re-render)://   serve una chiave di idempotenza generata dal CLIENT. Il server//   riconosce la ripetizione e restituisce lo stesso risultato.let inCorso = false;bottone.addEventListener('click', async () => {  if (inCorso) return;  inCorso = true;  bottone.disabled = true;  try {    await fetch('/api/ordini', {      method: 'POST',      headers: { 'Idempotency-Key': chiaveOrdine },   // stabile per QUESTO ordine      body: ...    });  } finally {    inCorso = false;    bottone.disabled = false;  }});// Lato server: la chiave è UNIQUE in tabella. La seconda richiesta// trova il record già creato e restituisce quello, senza duplicare.

Il doppio clic sul pulsante «conferma ordine» è la versione quotidiana dello stesso problema, e la soluzione istintiva — disabilitare il pulsante — è insufficiente: il secondo clic può partire prima che il ridisegno abbia effetto, e comunque non copre il caso in cui l'utente ricarichi la pagina e riprovi.

La difesa robusta è una chiave di idempotenza generata dal client e stabile per quel tentativo: il server la memorizza con vincolo di unicità, e alla seconda richiesta con la stessa chiave restituisce il risultato della prima invece di creare un secondo ordine. È il meccanismo che usano tutti i sistemi di pagamento seri, per lo stesso motivo.

Responsabilità singola: la domanda è «chi chiede la modifica»#

ordine.jsJavaScript
// Il principio di responsabilità singola non dice "funzioni corte".// Dice: un modulo deve avere UNA ragione per cambiare.// La domanda operativa è "chi chiede la modifica?".// ✗ Tre committenti diversi sullo stesso file: il commerciale (il prezzo),//   il grafico (il layout dell'email), il sistemista (il fornitore SMTP).//   Ogni loro richiesta rischia di rompere il lavoro degli altri.class Ordine {  calcolaTotale() { ... }        // regole commerciali  generaHtmlEmail() { ... }      // presentazione  inviaEmail() { ... }           // infrastruttura  salva() { ... }                // persistenza}// ✓ Separati per COMMITTENTE, non per dimensione del file.class Ordine { calcolaTotale() { ... } }        // cambia se cambia il listinoclass EmailOrdine { render(ordine) { ... } }    // cambia se cambia la graficaclass Postino { invia(messaggio) { ... } }      // cambia se cambia il fornitoreclass OrdiniRepo { salva(ordine) { ... } }      // cambia se cambia lo schema

Il principio viene quasi sempre riassunto come «una funzione, una cosa», che è un consiglio inutile perché «una cosa» non ha una definizione. La formulazione originale è più utile: un modulo dovrebbe avere una sola ragione per cambiare — e la ragione ha un nome e un cognome.

Nella classe di partenza convivono tre committenti: il responsabile commerciale che cambia il listino, il grafico che cambia il modello dell'email, il sistemista che cambia il fornitore SMTP. Ogni loro richiesta obbliga a toccare un file in cui vivono anche le altre due, con il rischio di romperle. Separare per committente elimina il rischio; separare per numero di righe non elimina niente.

Dove i principi si contraddicono#

conflitto.jsJavaScript
// Il conflitto che i manuali non affrontano: DRY e responsabilità singola// tirano in direzioni opposte, e chi cerca di soddisfarli entrambi al// massimo produce astrazioni che nessuno riesce più a seguire.// Queste due funzioni sono IDENTICHE oggi:function validaFattura(d)  { return d.importo > 0 && d.data <= oggi(); }function validaPreventivo(d) { return d.importo > 0 && d.data <= oggi(); }// Unificarle sembra DRY. Ma rispondono a due autorità diverse: la fattura// segue la normativa fiscale, il preventivo segue la politica commerciale.// Il giorno in cui i preventivi potranno avere data futura, l'astrazione// va smontata — e nel frattempo qualcuno ci avrà appeso altre tre cose.// Il criterio: DRY riguarda la CONOSCENZA, non il testo del codice.// Due frammenti identici che rispondono a due regole diverse non sono// una duplicazione: sono una coincidenza.

Questa è la parte che i manuali saltano. La responsabilità singola spinge a separare, DRY spinge ad accorpare, e applicati entrambi al massimo producono gerarchie di astrazioni in cui per capire cosa fa una funzione bisogna aprire sei file.

La riformulazione che scioglie il conflitto è nella definizione originale di DRY, che non parla di codice: ogni pezzo di conoscenza deve avere una rappresentazione unica e autorevole nel sistema. Conoscenza, non testo. Due funzioni identiche che rispondono a due autorità diverse non violano DRY: si somigliano per caso, e unificarle crea un accoppiamento fra cose che devono poter evolvere separatamente.

La regola dei tre, e perché tre#

quando astrarre
Il criterio operativo, e perché la soglia è tre:  1ª occorrenza  → si scrive.  2ª occorrenza  → si copia. Non si sa ancora COSA varia fra le due,                   e un'astrazione costruita su un solo confronto                   indovina il punto di variazione quasi sempre male.  3ª occorrenza  → adesso la forma comune si vede: due assi di                   variazione confermati, non ipotizzati. Si astrae.Il costo asimmetrico che giustifica l'attesa:  - duplicazione di troppo → si trova con una ricerca testuale  - astrazione sbagliata   → tre chiamanti accoppiati, un parametro                             booleano che ne governa il comportamento,                             e nessuno che osi toccarla

La ragione per cui la soglia è tre e non due è che con due esempi non si sa ancora cosa varia. Un'astrazione costruita su un solo confronto indovina l'asse di variazione quasi sempre male, e il modo in cui sbaglia è riconoscibile:

sintomo.jsJavaScript
// Come si riconosce un'astrazione sbagliata: il parametro booleano che// seleziona il comportamento. È il segnale che dentro una funzione ce// ne sono due, tenute insieme dal fatto che si somigliavano.function invia(dest, testo, isUrgente, isHtml, saltaCoda) { ... }//                          ~~~~~~~~~  ~~~~~~  ~~~~~~~~~// invia(x, y, true, false, true)  ← illeggibile al punto di chiamata// Quando compaiono, la strada di solito è tornare indietro: separare in// funzioni con un nome, e accettare la duplicazione che ne deriva.// "Duplication is far cheaper than the wrong abstraction" — Sandi Metz

Il parametro booleano che seleziona il comportamento è il sintomo classico: dentro quella funzione ci sono due funzioni diverse, tenute insieme dal fatto che un giorno si somigliavano. Al punto di chiamata, invia(x, y, true, false, true) non è leggibile da nessuno.

Una sola fonte di verità#

derivati.jsJavaScript
// Una sola fonte di verità, in pratica: il valore derivato non si// memorizza, si calcola. Quello che non esiste non può divergere.// ✗ Due copie della stessa informazione: prima o poi non coincidono//   (un ordine annullato che non decrementa il totale, un'operazione//   fallita a metà, una modifica fatta a mano sul database)const ordine = { righe: [...], totale: 1250, numeroRighe: 4 };// ✓ Una sola, e le altre derivanoconst ordine = { righe: [...] };const totale = ordine.righe.reduce((s, r) => s + r.prezzo * r.qta, 0);// L'eccezione legittima è la denormalizzazione per prestazioni: si// memorizza il derivato perché ricalcolarlo costa troppo. In quel caso// va dichiarato esplicitamente e va garantito che l'aggiornamento// avvenga nella STESSA transazione — non "da qualche parte, dopo".

È DRY applicato ai dati invece che al codice, ed è il principio che previene la categoria di bug più difficile da diagnosticare: quelli in cui due parti del sistema hanno ragione entrambe, perché guardano due copie che hanno smesso di coincidere.

L'eccezione legittima è la denormalizzazione per prestazioni — ricalcolare costa troppo, quindi si memorizza. Va però trattata come un'eccezione dichiarata: il valore derivato è documentato come tale, e il suo aggiornamento avviene nella stessa transazione che modifica la fonte. «Lo aggiorniamo dopo, in un job» è il modo in cui si costruiscono divergenze permanenti.

Segnale nel codiceCosa indicaDirezione
SELECT, controllo, UPDATErace conditioncondizione dentro la scrittura
Endpoint che crea senza chiaveduplicati da doppio inviochiave di idempotenza + vincolo unico
Un file toccato da tre persone per motivi diversitroppe responsabilitàseparare per committente
Parametro booleano che sceglie il comportamentoastrazione sbagliatatornare indietro e separare
Lo stesso numero in due tabellefonti di verità multiplecalcolare, o aggiornare in transazione
La stessa regola in client e serverduplicazione verauna sola definizione, condivisa
Sei file da aprire per capire una funzioneeccesso di astrazioneappiattire

La sintesi che uso: i principi non sono regole da applicare, sono nomi per problemi ricorrenti. Servono a riconoscere una situazione più in fretta, non a decidere al posto vostro. La decisione richiede di sapere quale cambiamento è probabile che arrivi — e quella è conoscenza del dominio, non del linguaggio.