Rich Hickey & Clojure: semplicità, immutabilità, default migliori
Uno sguardo accessibile alle idee di Rich Hickey su Clojure: semplicità, immutabilità e default migliori—lezioni pratiche per costruire sistemi complessi più calmi e sicuri.

Perché la complessità continua a vincere nei progetti reali
Il software raramente diventa complicato tutto in una volta. Ci arriva con una decisione “ragionevole” alla volta: una cache veloce per rispettare una scadenza, un oggetto mutabile condiviso per evitare copie, un’eccezione alle regole perché “questo è speciale”. Ogni scelta sembra piccola, ma insieme creano un sistema in cui i cambiamenti sembrano rischiosi, i bug sono difficili da riprodurre e aggiungere funzionalità richiede più tempo che costruirle.
La complessità vince perché offre conforto a breve termine. Spesso è più veloce collegare una nuova dipendenza che semplificare una esistente. È più semplice patchare lo stato che chiedersi perché lo stato è distribuito su cinque servizi. E quando il sistema cresce più velocemente della documentazione, è facile affidarsi a convenzioni e alla conoscenza tribale.
Cosa è (e cosa non è) questo articolo
Questo non è un tutorial su Clojure, e non devi conoscere Clojure per ottenere valore. L’obiettivo è prendere in prestito un insieme di idee pratiche spesso associate al lavoro di Rich Hickey—idee che puoi applicare alle decisioni di ingegneria quotidiane, indipendentemente dal linguaggio.
Perché i default contano più di quanto pensi
La maggior parte della complessità non nasce dal codice che scrivi deliberatamente; nasce da ciò che i tuoi strumenti rendono facile di default. Se il default è “oggetti mutabili ovunque”, ti ritroverai con accoppiamenti nascosti. Se il default è “lo stato vive in memoria”, farai fatica con il debugging e la tracciabilità. I default formano abitudini, e le abitudini formano i sistemi.
Ci concentreremo su tre temi:
- Semplicità: non meno funzioni, ma meno parti in movimento e meno casi speciali.
- Immutabilità: trattare i dati come valori che non cambiano, così puoi ragionarci meglio.
- Default migliori: scegliere pattern che rendano l’opzione sicura e prevedibile la più semplice.
Queste idee non eliminano la complessità dal tuo dominio, ma possono impedire al software di moltiplicarla.
Rich Hickey e cosa Clojure voleva risolvere
Rich Hickey è uno sviluppatore e designer con molta esperienza, noto soprattutto per aver creato Clojure e per i suoi talk che mettono in discussione abitudini comuni nella programmazione. Il suo focus non è sulle mode—è sui motivi ricorrenti per cui i sistemi diventano difficili da cambiare, da capire e da fidarsi una volta cresciuti.
Cos'è Clojure (alto livello, senza gergo)
Clojure è un linguaggio di programmazione moderno che gira su piattaforme ben note come la JVM (runtime di Java) e JavaScript. È pensato per lavorare con ecosistemi esistenti incoraggiando però uno stile specifico: rappresentare l’informazione come dati semplici, preferire valori che non cambiano e tenere separato il “cosa è successo” da “cosa mostri sullo schermo”.
Lo puoi vedere come un linguaggio che ti spinge verso mattoni costruttivi più chiari e lontano dagli effetti collaterali nascosti.
I problemi che voleva ridurre
Clojure non è stato creato per rendere più brevi i piccoli script. È nato per affrontare dolori ricorrenti nei progetti:
- Crescente complessità dallo stato condiviso: quando molte parti di un sistema possono modificare gli stessi dati, i bug diventano dipendenti dal timing e difficili da riprodurre.
- Accoppiamento stretto tra dati e comportamento: quando l’informazione è bloccata dentro oggetti o classi, il riuso diventa più difficile e il cambiamento si propaga nel codice.
- Problemi di concorrenza: mano a mano che i sistemi aggiungono job in background, code e lavoro parallelo, “chi ha cambiato cosa e quando?” diventa un problema quotidiano.
I default di Clojure spingono verso meno parti in movimento: strutture dati stabili, aggiornamenti espliciti e strumenti che rendono la coordinazione più sicura.
Utile anche se non adotterai mai Clojure
Il valore non si limita al cambio di linguaggio. Le idee centrali di Hickey—semplificare rimuovendo interdipendenze inutili, trattare i dati come fatti duraturi e minimizzare lo stato mutabile—possono migliorare sistemi in Java, Python, JavaScript e oltre.
Semplicità: non “facile”, ma meno parti in movimento
Rich Hickey traccia una linea netta tra semplice e facile—e molti progetti la oltrepassano senza accorgersene.
Semplice vs. facile (con esempi quotidiani)
Facile riguarda come qualcosa si sente ora. Semplice riguarda quante parti ha e quanto sono intrecciate.
- I noodles istantanei sono facili. Uno stufato di base può essere semplice: pochi ingredienti, una pentola, niente di nascosto.
- Un telecomando con 60 pulsanti può rendere una funzione “facile” (è lì), ma non è semplice. Un telecomando con 6 controlli chiari è più semplice, anche se richiede un minuto per impararlo.
Nel software, “facile” spesso significa “veloce da digitare oggi”, mentre “semplice” significa “più difficile da rompere il mese prossimo”.
Come il “facile ora” crea complessità futura
I team spesso scelgono scorciatoie che riducono l’attrito immediato ma aggiungono struttura invisibile che va mantenuta:
- “Aggiungiamo solo un flag.” Ora ogni funzione deve considerare quel flag.
- “Memorizziamo il valore calcolato per risparmiare tempo.” Ora devi mantenerlo sincronizzato in più cammini.
- “Lo correggiamo nell’UI.” Ora la stessa regola di business esiste in più posti.
Ogni scelta può sembrare velocità, ma aumenta il numero di parti in movimento, i casi speciali e le dipendenze incrociate. È così che i sistemi diventano fragili senza un singolo errore clamoroso.
Velocità non è uguale a semplicità
Rilasciare in fretta può essere ottimo—ma velocità senza semplificare significa spesso che stai prendendo in prestito dal futuro. Gli interessi si manifestano come bug difficili da riprodurre, onboarding che rallenta e cambiamenti che richiedono “coordinazione attenta”.
Una checklist rapida per la complessità accidentale
Fai queste domande quando esamini un design o una PR:
- Abbiamo introdotto nuove modalità, flag o rami di configurazione?
- Stiamo mettendo in cache o duplicando dati che devono restare consistenti?
- Più moduli devono cambiare insieme per un comportamento?
- La regola è implementata in più di un posto?
- Un nuovo membro prevederebbe come funziona senza spiegazioni extra?
Stato: il moltiplicatore silenzioso di complessità
Con “stato” intendiamo semplicemente le cose nel tuo sistema che possono cambiare: il carrello di un utente, il saldo di un account, la configurazione corrente, il passo di un workflow. La parte difficile non è che il cambiamento esista—è che ogni cambiamento crea una nuova opportunità che le cose non coincidano.
Quando si dice “lo stato causa bug”, di solito si intende questo: se la stessa informazione può essere diversa in momenti diversi (o in posti diversi), il tuo codice deve costantemente rispondere: “Quale versione è quella vera adesso?” Sbagliare questa risposta produce errori che sembrano casuali.
Mutabilità: cambiamenti che non puoi ignorare
La mutabilità significa che un oggetto viene modificato in luogo: la “stessa” cosa diventa diversa nel tempo. Sembra efficiente, ma rende il ragionamento più difficile perché non puoi più fare affidamento su ciò che hai visto un attimo prima.
Un esempio familiare è un foglio di calcolo condiviso. Se più persone possono modificare le stesse celle contemporaneamente, la tua comprensione può essere invalidata: i totali cambiano, le formule si interrompono o una riga scompare perché qualcuno l'ha riorganizzata. Anche senza intenzioni malevole, la natura condivisa e modificabile genera confusione.
Lo stato nel software si comporta allo stesso modo. Se due parti di un sistema leggono lo stesso valore mutabile, una parte può modificarlo silenziosamente mentre l’altra continua con un’assunzione obsoleta.
Perché il debugging diventa così doloroso
Lo stato mutabile trasforma il debugging in archeologia. Un report di bug raramente dice “i dati sono stati modificati in modo errato alle 10:14:03.” Vedi solo il risultato finale: un numero sbagliato, uno stato inatteso, una richiesta che fallisce solo a volte.
Poiché lo stato cambia nel tempo, la domanda più importante diventa: quale sequenza di modifiche ha portato qui? Se non puoi ricostruire quella storia, il comportamento diventa imprevedibile:
- La stessa azione produce risultati diversi a seconda del timing.
- Le correzioni “funzionano sulla mia macchina” ma non in produzione.
- Aggiungere logging cambia i tempi e il bug scompare.
Ecco perché Hickey tratta lo stato come un moltiplicatore di complessità: una volta che i dati sono sia condivisi che mutabili, il numero di possibili interazioni cresce più rapidamente della tua capacità di tenerle a mente.
Immutabilità spiegata senza gergo informatico
L'immutabilità significa semplicemente dati che non cambiano dopo essere stati creati. Invece di prendere una informazione esistente e modificarla in loco, crei una nuova informazione che riflette l'aggiornamento.
Pensa a una ricevuta: una volta stampata, non cancelli le righe e riscrivi i totali. Se qualcosa cambia, emetti una ricevuta corretta. La vecchia rimane, e la nuova è chiaramente “la versione più recente”.
Perché riduce le sorprese
Quando i dati non possono essere modificati di nascosto, smetti di preoccuparti delle modifiche invisibili alle tue spalle. Questo facilita il ragionamento quotidiano:
- Se tieni un valore, puoi fidarti che rimarrà così.
- I bug sono più facili da riprodurre perché lo stesso input resta uguale.
- Condividere dati tra parti diverse è più sicuro perché nessuno può “rovinare” accidentalmente i dati per qualcun altro.
Questa è una parte importante del motivo per cui Hickey parla di semplicità: meno effetti collaterali nascosti significa meno rami mentali da tenere a mente.
“Nuove versioni” vs “modificare in luogo”
Creare nuove versioni può sembrare dispendioso finché non confronti l’alternativa. Modificare in luogo ti lascia a chiederti: “Chi ha cambiato questo? Quando? Com’era prima?” Con i dati immutabili, le modifiche sono esplicite: esiste una nuova versione e la vecchia rimane disponibile per debugging, audit o rollback.
Clojure favorisce questo approccio rendendo naturale trattare gli aggiornamenti come produzione di nuovi valori, non mutazioni di quelli vecchi.
Compromessi da riconoscere
L’immutabilità non è gratis. Potresti allocare più oggetti e i team abituati a “aggiornare la cosa” potrebbero aver bisogno di tempo per adattarsi. La buona notizia è che le implementazioni moderne spesso condividono strutture internamente per ridurre il costo in memoria, e il vantaggio è quasi sempre sistemi più calmi con meno incidenti difficili da spiegare.
La concorrenza è più semplice quando i dati non cambiano
La concorrenza è semplicemente “molte cose che succedono insieme”. Un’app web che gestisce migliaia di richieste, un sistema di pagamento che aggiorna saldi mentre genera ricevute, o un’app mobile che fa sync in background—tutti questi sono esempi.
La difficoltà non è che più cose accadono. È che spesso toccano gli stessi dati.
Perché i dati condivisi e modificabili creano race condition
Quando due worker possono leggere e poi modificare lo stesso valore, il risultato finale può dipendere dal timing. Questa è una race condition: un bug che non si riproduce facilmente, ma appare quando il sistema è sotto carico.
Esempio: due richieste provano ad aggiornare un totale d’ordine.
- Richiesta A legge total = 100
- Richiesta B legge total = 100
- A aggiunge 20 e scrive 120
- B aggiunge 10 e scrive 110
Niente è “andato in crash”, ma hai perso un aggiornamento. Sotto carico, queste finestre temporali diventano più comuni.
Le soluzioni tradizionali—lock, blocchi sincronizzati, ordinamenti attenti—funzionano, ma costringono tutti a coordinarsi. La coordinazione è costosa: rallenta il throughput e diventa fragile con la crescita del codice.
Come l'immutabilità riduce al minimo la coordinazione
Con dati immutabili, un valore non viene modificato in luogo. Invece, crei un nuovo valore che rappresenta il cambiamento.
Questo singolo spostamento elimina un’intera categoria di problemi:
- I lettori non devono temere che ciò che guardano cambi a metà lettura.
- Gli scrittori non “combattono” per la stessa memoria; producono nuove versioni.
- Il sistema può scegliere modi sicuri per pubblicare la versione più recente (spesso con primitive semplici e ben testate).
Il risultato: comportamento prevedibile sotto carico
L'immutabilità non rende la concorrenza gratuita—serve comunque una regola su quale versione è corrente. Ma rende i programmi concorrenti molto più prevedibili, perché i dati non sono un bersaglio mobile. Quando il traffico sale o i job in background si accumulano, è meno probabile vedere fallimenti misteriosi dipendenti dal timing.
Cosa significano “default migliori” nella pratica
“Default migliori” significa che la scelta più sicura accade automaticamente, e ti assumi un rischio in più solo quando scegli esplicitamente di farlo.
Sembra una cosa piccola, ma i default guidano ciò che le persone scrivono il lunedì mattina, ciò che i reviewer accettano il venerdì pomeriggio e ciò che un nuovo membro impara dal primo codice che tocca.
Default che riducono il rischio
Un “default migliore” non è decidere tutto per te. È rendere il percorso comune meno incline all’errore.
Per esempio:
- Dati immutabili come default: invece di “modificare la cosa”, crei una nuova versione. Questo rende più difficile influenzare accidentalmente altre parti del programma che si basavano sul valore precedente.
- Funzioni pure come stile normale: una funzione prende input e restituisce output senza cambiare dati condivisi o dipendere da stato globale nascosto. Questo rende il comportamento più prevedibile e testabile.
- Cambiamenti di stato espliciti: quando qualcosa deve cambiare, avviene tramite meccanismi chiari e ben definiti (piuttosto che permettere a qualsiasi parte del codice di mutare tutto).
Niente di tutto ciò elimina la complessità, ma impedisce che si diffonda.
Come i default plasmano team e review
I team non seguono solo la documentazione—seguono ciò che il codice “ti vuole far fare”.
Quando mutare lo stato condiviso è facile, diventa una scorciatoia normale, e i reviewer si ritrovano a discutere l’intento: “È sicuro qui?” Quando immutabilità e funzioni pure sono default, i reviewer possono concentrarsi su logica e correttezza, perché le mosse rischiose si notano subito.
In altre parole, i default migliori creano una linea di base più sana: la maggior parte dei cambiamenti appare coerente e i pattern insoliti sono abbastanza evidenti da essere messi in discussione.
Manutenzione e onboarding
La manutenzione a lungo termine riguarda principalmente leggere e modificare codice esistente in sicurezza.
I default migliori aiutano i nuovi arrivati a salire a bordo perché ci sono meno regole nascoste (“attento, questa funzione aggiorna segretamente quella mappa globale”). Il sistema diventa più facile da ragionare, il che abbassa il costo di ogni futura feature, fix e refactor.
Separa i fatti dalle viste: tempo, cronologia e tracciabilità
Un cambio mentale utile nei talk di Hickey è separare i fatti (ciò che è successo) dalle viste (ciò che al momento crediamo essere vero). Molti sistemi confondono questi concetti conservando solo il valore più recente—sovrascrivendo ieri con oggi—e questo fa sparire il tempo.
I fatti sono append-only; le viste sono derivate
Un fatto è un record immutabile: “Ordine #4821 creato alle 10:14”, “Pagamento riuscito”, “Indirizzo modificato”. Questi non vengono editati; aggiungi nuovi fatti man mano che la realtà cambia.
Una vista è ciò di cui la tua app ha bisogno ora: “Qual è l’indirizzo di spedizione corrente?” o “Qual è il saldo del cliente?” Le viste possono essere ricalcolate dai fatti, messe in cache, indicizzate o materializzate per velocità.
Perché conservare la storia ripaga
Quando conservi i fatti, ottieni:
- Auditabilità: puoi spiegare perché il valore corrente è quello che è.
- Debugging: puoi riprodurre la sequenza e trovare il momento in cui le cose hanno diverged.
- Tracciabilità: “Chi ha cambiato questo, quando e com’era prima?” diventa una domanda sui dati, non un’indagine da detective.
Un esempio semplice: sovrascrivere vs aggiungere
Sovrascrivere i record è come aggiornare una cella di un foglio di calcolo: vedi solo il numero più recente.
Un log append-only è come il registro di un libretto degli assegni: ogni voce è un fatto e il “saldo corrente” è una vista calcolata dalle voci.
Non tutti i sistemi hanno bisogno di event sourcing completo
Non devi adottare un’architettura event-sourced completa per beneficiare. Molti team iniziano in piccolo: mantieni una tabella di audit append-only per cambi critici, conserva eventi immutabili per alcuni workflow ad alto rischio o conserva snapshot più una finestra di storia limitata. L’abitudine chiave è: tratta i fatti come duraturi e lo stato corrente come una proiezione comoda.
Data-first: rendi l’informazione durevole e flessibile
Un’idea pratica di Hickey è data first: tratta le informazioni del sistema come valori semplici (fatti) e tratta il comportamento come qualcosa che esegui su quei valori.
I dati sono durevoli. Se memorizzi informazioni chiare e autonome, puoi reinterpretarle in seguito, spostarle tra servizi, reindicizzarle, auditarle o alimentarle in nuove funzionalità. Il comportamento è meno durevole—il codice cambia, le assunzioni cambiano, le dipendenze cambiano.
Valori vs azioni (senza gergo)
- Dati (valori): “Cosa è vero?” L’email di un cliente, il totale di un ordine, un timestamp, uno stato.
- Comportamento (azioni): “Cosa facciamo?” Validare, calcolare sconti, inviare notifiche, decidere cosa significa uno stato.
Quando mescoli questi, i sistemi diventano appiccicosi: non puoi riusare i dati senza trascinarti dietro il comportamento che li ha creati.
Meno accoppiamento, più riuso
Separare fatti e azioni riduce l’accoppiamento perché i componenti possono accordarsi su una forma dei dati senza accordarsi su un percorso di codice condiviso.
Un job di reporting, uno strumento di supporto e un servizio di fatturazione possono consumare gli stessi dati d’ordine, applicando ciascuno la propria logica. Se incapsuli la logica nella rappresentazione memorizzata, ogni consumer diventa dipendente da quella logica—e cambiarla diventa rischioso.
Esempio: salvare dati puliti vs mini-programmi
Dati puliti (facili da evolvere):
{
"type": "discount",
"code": "WELCOME10",
"percent": 10,
"valid_until": "2026-01-31"
}
Mini-programmi nello storage (difficili da evolvere):
{
"type": "discount",
"rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}
La seconda versione sembra flessibile, ma spinge la complessità nel livello dati: ora hai bisogno di un evaluator sicuro, regole di versioning, confini di sicurezza, strumenti di debug e un piano di migrazione quando il linguaggio delle regole cambia.
Perché questo rende i sistemi evolvibili
Quando l’informazione memorizzata rimane semplice ed esplicita, puoi cambiare il comportamento nel tempo senza riscrivere la storia. I vecchi record rimangono leggibili. Nuovi servizi possono essere aggiunti senza “capire” regole esecutive legacy. E puoi introdurre nuove interpretazioni—nuove viste UI, nuove strategie di pricing, nuove analisi—scrivendo nuovo codice, non mutando ciò che i tuoi dati significano.
Applicare le idee ai sistemi complessi (senza riscrivere tutto)
La maggior parte dei sistemi enterprise non fallisce perché un modulo è “cattivo”. Fallisce perché tutto è connesso a tutto.
I modi di rottura a cui prestare attenzione
L’accoppiamento stretto si manifesta come cambiamenti “piccoli” che scatenano settimane di retesting. Un campo aggiunto a un servizio rompe tre consumer a valle. Uno schema di database condiviso diventa un collo di bottiglia per il coordinamento. Una cache mutabile o un singleton di “config” diventa silenziosamente una dipendenza di metà codebase.
Il cambiamento a cascata è il risultato naturale: quando molte parti condividono la stessa cosa che cambia, il raggio d’azione si espande. I team rispondono aggiungendo più processi, più regole e più passaggi—spesso rendendo le consegne ancora più lente.
Riduci il raggio d’azione con confini più semplici
Puoi applicare le idee di Hickey senza cambiare linguaggi o riscrivere tutto:
- Preferisci dati immutabili ai confini. Tratta messaggi, eventi e input API come “fatti” che non vengono editati in luogo. Se qualcosa cambia, crea una nuova versione.
- Sposta lo stato ai bordi. Mantieni la logica centrale come trasformazioni pure: input data → output data. Lascia a database, cache e UI il ruolo di gestire lo “stato corrente”.
- Smetti di condividere strutture mutabili. Se due moduli scrivono lo stesso oggetto, sono accoppiati. Passa valori, non riferimenti che intendi mutare.
Quando i dati non cambiano sotto i tuoi piedi, passi meno tempo a fare debug su “come è arrivato in questo stato?” e più tempo a ragionare su cosa fa il codice.
“Default migliori” tra i team
I default sono dove si annida l’inconsistenza: ogni team inventa il proprio formato di timestamp, forma di errore, politica di retry e approccio alla concorrenza.
Default migliori sembrano: schemi di eventi versionati, DTO immutabili standard, proprietà chiare delle scritture e una piccola selezione di librerie approvate per serializzazione, validazione e tracing. Il risultato è meno integrazioni sorprese e meno fix one-off.
Adozione incrementale che funziona nel codice esistente
Inizia dove il cambiamento è già in corso:
- Avvolgi i moduli esistenti con un’API che accetta/restiuisce dati immutabili.
- Converte un workflow ad alto churn in record “append-only”.
- Sostituisci cache condivise mutabili con viste ricalcolabili da fatti durevoli.
Questo approccio migliora affidabilità e coordinazione del team mantenendo il sistema in funzione—e mantiene lo scope abbastanza piccolo da poter essere completato.
Dove piattaforme e tooling possono rinforzare i “default migliori”
È più facile applicare queste idee quando il tuo workflow supporta iterazioni rapide e a basso rischio. Per esempio, se costruisci nuove feature in Koder.ai (una piattaforma chat-based vibe-coding per web, backend e app mobile), due funzionalità mappano direttamente sulla mentalità dei “default migliori”:
- Planning mode ti incoraggia a rendere espliciti confini e forme dei dati prima dell’implementazione—spesso la differenza tra un flusso dati semplice e un accoppiamento accidentale.
- Snapshot e rollback rendono più sicuro il rilascio, perché puoi revertare velocemente quando una scorciatoia “facile” diventa un picco di complessità.
Anche se il tuo stack è React + Go + PostgreSQL (o Flutter per mobile), il punto centrale rimane: gli strumenti che usi quotidianamente insegnano silenziosamente un modo di lavorare. Scegli strumenti che rendono routine la tracciabilità, il rollback e la pianificazione esplicita per ridurre la pressione di “patchare al volo”.
Compromessi, limiti ed evitare l’ideologia
Semplicità e immutabilità sono default potenti, non regole morali. Ridurre il numero di cose che possono cambiare inaspettatamente aiuta quando i sistemi crescono. Ma i progetti reali hanno budget, scadenze e vincoli—e a volte la mutabilità è lo strumento giusto.
Quando la mutabilità è accettabile
La mutabilità può essere una scelta pratica in hotspot di prestazioni (cicli stretti, parsing ad alto throughput, grafica, lavoro numerico) dove le allocazioni dominano. Va bene anche quando l’ambito è controllato: variabili locali dentro una funzione, una cache privata nascosta dietro un’interfaccia o un componente single-thread con confini chiari.
La parola chiave è contenimento. Se la “cosa mutabile” non trapela, non può diffondere complessità nell’intera codebase.
Limita la complessità con proprietà e interfacce
Anche in uno stile per lo più funzionale, i team hanno comunque bisogno di proprietà chiare:
- Un modulo possiede uno stato e espone una piccola API.
- I dati attraversano i confini come valori semplici (non oggetti vivi con comportamento segreto).
- Gli effetti collaterali sono spinti ai margini (I/O, DB, tempo).
Qui l’inclinazione di Clojure verso i dati e i confini espliciti aiuta, ma la disciplina è architetturale, non legata al linguaggio.
Cosa Clojure non risolverà
Nessun linguaggio risolve requisiti poveri, un modello di dominio confuso o un team che non si mette d’accordo su cosa significhi “done”. L’immutabilità non rende comprensibile un workflow confuso, e il codice “funzionale” può comunque codificare regole di business sbagliate—solo in modo più ordinato.
Evita il dogma: riduci il rischio con la minima modifica
Se il tuo sistema è già in produzione, non trattare queste idee come un rewrite totale. Cerca la modifica minima che riduce il rischio:
- Sostituisci strutture mutabili condivise con dati immutabili ai confini dei moduli.
- Aggiungi log di eventi append-only dove l’auditabilità conta.
- Avvolgi lo stato legacy dietro un’interfaccia stretta e impedisci che si diffonda.
L’obiettivo non è la purezza—è meno sorprese a ogni cambiamento.
Una checklist pratica per muoversi verso software più semplice
Questa è una checklist da sprint che puoi applicare senza cambiare linguaggi, framework o struttura del team.
3–5 cose da provare nel prossimo sprint
-
Rendi le “forme dei dati” immutabili per default. Tratta oggetti di richiesta/risposta, eventi e messaggi come valori che crei una volta e non modifichi mai. Se qualcosa deve cambiare, crea una nuova versione.
-
Preferisci funzioni pure nel mezzo dei workflow. Inizia con un workflow (es. pricing, permessi, checkout) e rifattorizza il core in funzioni che prendono dati e restituiscono dati—niente letture/scritture nascoste.
-
Sposta lo stato in posti meno numerosi e più chiari. Scegli una singola fonte di verità per concetto (stato cliente, feature flag, inventario). Se più moduli mantengono copie proprie, rendilo una decisione esplicita con una strategia di sincronizzazione.
-
Aggiungi un log append-only per fatti chiave. Per un’area di dominio, registra “cosa è successo” come eventi durevoli (anche se continui a memorizzare lo stato corrente). Questo migliora tracciabilità e riduce le congetture.
-
Definisci default più sicuri nelle API. I default dovrebbero minimizzare comportamenti sorprendenti: timezone espliciti, gestione esplicita dei null, retry espliciti, garanzie di ordinamento esplicite.
Domande da porre in una design review (usa queste testualmente)
- Quali sono le parti mutabili qui e chi può cambiarle?
- Possiamo modellare questo come “fatti” più una vista derivata invece di sovrascrivere campi?
- Se due richieste corrono contemporaneamente, cosa si rompe?
- Su quali default facciamo affidamento (tempo, ordinamento, retry, caching) e sono documentati?
- Di cosa avremmo bisogno per fare debug fra tre mesi?
Argomenti suggeriti dai talk di Hickey da rivedere
Cerca materiale su semplicità vs facilità, gestione dello stato, design orientato ai valori, immutabilità e come la “storia” (fatti nel tempo) aiuta debugging e operazioni.
La semplicità non è una caratteristica che aggiungi—è una strategia che pratichi con scelte piccole e ripetibili.
Domande frequenti
Perché la complessità tende a “vincere” nei progetti reali?
La complessità si accumula attraverso piccole decisioni localmente ragionevoli (flag in più, cache, eccezioni, helper condivisi) che aggiungono modalità e accoppiamenti.
Un buon indicatore è quando una “piccola modifica” richiede modifiche coordinate in più moduli o servizi, oppure quando i reviewer devono affidarsi a conoscenza tribale per giudicare la sicurezza.
Qual è la differenza tra “semplice” e “facile” nel software?
Perché le scorciatoie ottimizzano per la frizione di oggi (tempo di rilascio) mentre spostano i costi nel futuro: tempo di debug, overhead di coordinamento e rischio di cambiamento.
Un’abitudine utile è chiedersi nella revisione di design/PR: “Quali nuovi pezzi in movimento o casi speciali introduce questo, e chi li manterrà?”
In che modo le impostazioni predefinite di linguaggi e framework creano complessità accidentale?
Le impostazioni predefinite guidano ciò che gli ingegneri fanno sotto pressione. Se la mutazione è il comportamento predefinito, lo stato condiviso si diffonde. Se “in memoria va bene” è la predefinizione, la tracciabilità scompare.
Migliora i default facendo sì che la strada sicura sia la più semplice da intraprendere: dati immutabili ai confini, timezone/null espliciti, retry espliciti e proprietà dello stato chiaramente definite.
Perché lo stato è descritto come un “moltiplicatore di complessità”?
Lo stato è tutto ciò che cambia nel tempo. La difficoltà è che il cambiamento crea opportunità di disaccordo: due componenti possono avere valori “correnti” diversi.
I bug emergono come comportamenti dipendenti dal timing (“funziona in locale”, problemi flakiness in produzione) perché la domanda diventa: su quale versione dei dati abbiamo agito?
Cosa significa l'immutabilità in termini pratici e non accademici?
L'immutabilità significa che non modifichi un valore in loco: crei un nuovo valore che rappresenta l'aggiornamento.
Praticamente, aiuta perché:
- I lettori possono fidarsi che i dati non cambieranno mentre li usano.
- Riprodurre i bug è più semplice (gli input rimangono stabili).
- Condividere dati tra thread/moduli è più sicuro.
Quando la mutabilità è accettabile (o addirittura preferibile)?
Non sempre è sbagliata. La mutabilità può essere una buona scelta quando è contenuta:
- Variabili locali dentro una funzione
- Hotspot di prestazioni (cicli stretti, parsing, calcolo numerico)
- Cache private dietro un’interfaccia ristretta
La regola chiave: non lasciare che le strutture mutabili trapelino oltre i confini dove molte parti possono leggere/scrivere.
In che modo l'immutabilità aiuta con la concorrenza sotto carico?
Le race condition nascono tipicamente da dati condivisi e mutabili letti e poi scritti da più worker.
L'immutabilità riduce la superficie di coordinamento perché gli scrittori producono nuove versioni invece di modificare un oggetto condiviso. Serve comunque una regola per pubblicare quale versione è corrente, ma i dati smettono di essere un bersaglio mobile.
Cosa significa separare “fatti” e “viste” e come posso applicarlo in modo incrementale?
Considera i fatti come record append-only di ciò che è successo (eventi) e lo “stato corrente” come una vista derivata da quei fatti.
Puoi iniziare in piccolo senza un’architettura event-sourced completa:
- Aggiungi una tabella di audit per i cambi critici
- Registra eventi di cambiamento per un workflow ad alto rischio
- Conserva snapshot più una finestra di storia limitata per replay/debug
Cos'è il design “data-first” e perché riduce l'accoppiamento?
Conserva le informazioni come dati semplici ed espliciti (valori) e applica la logica su quei dati. Evita di incorporare regole eseguibili nei record memorizzati.
Questo rende i sistemi più evolvibili perché:
- I record vecchi restano leggibili anche se il codice cambia
- Nuovi consumatori possono riutilizzare la stessa forma di dato
- Puoi cambiare la logica senza riscrivere la storia
Quali sono le prime 3 modifiche concrete da provare nel prossimo sprint per ridurre la complessità?
Scegli un workflow che cambia spesso e applica tre passi:
- Rendi immutabili i dati ai confini: tratta input/output API, messaggi ed eventi come “crea una volta, non mutate”.
- Rifattorizza il core in trasformazioni pure: dati in ingresso → dati in uscita; sposta I/O e effetti collaterali ai margini.
- Riduci le strutture mutabili condivise: un unico proprietario per ogni pezzo di stato, esposto tramite una piccola interfaccia.
Misura il successo con meno bug flakiness, minore raggio d’azione per ogni cambiamento e meno “coordinazione attenta” nei rilasci.