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.

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.
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.
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:
Queste idee non eliminano la complessità dal tuo dominio, ma possono impedire al software di moltiplicarla.
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.
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.
Clojure non è stato creato per rendere più brevi i piccoli script. È nato per affrontare dolori ricorrenti nei progetti:
I default di Clojure spingono verso meno parti in movimento: strutture dati stabili, aggiornamenti espliciti e strumenti che rendono la coordinazione più sicura.
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.
Rich Hickey traccia una linea netta tra semplice e facile—e molti progetti la oltrepassano senza accorgersene.
Facile riguarda come qualcosa si sente ora. Semplice riguarda quante parti ha e quanto sono intrecciate.
Nel software, “facile” spesso significa “veloce da digitare oggi”, mentre “semplice” significa “più difficile da rompere il mese prossimo”.
I team spesso scelgono scorciatoie che riducono l’attrito immediato ma aggiungono struttura invisibile che va mantenuta:
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.
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”.
Fai queste domande quando esamini un design o una PR:
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.
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.
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:
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.
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”.
Quando i dati non possono essere modificati di nascosto, smetti di preoccuparti delle modifiche invisibili alle tue spalle. Questo facilita il ragionamento quotidiano:
Questa è una parte importante del motivo per cui Hickey parla di semplicità: meno effetti collaterali nascosti significa meno rami mentali da tenere a mente.
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.
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 è 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.
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.
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.
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:
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.
“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.
Un “default migliore” non è decidere tutto per te. È rendere il percorso comune meno incline all’errore.
Per esempio:
Niente di tutto ciò elimina la complessità, ma impedisce che si diffonda.
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.
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.
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.
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à.
Quando conservi i fatti, ottieni:
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 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.
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.
Quando mescoli questi, i sistemi diventano appiccicosi: non puoi riusare i dati senza trascinarti dietro il comportamento che li ha creati.
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.
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.
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.
La maggior parte dei sistemi enterprise non fallisce perché un modulo è “cattivo”. Fallisce perché tutto è connesso a tutto.
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.
Puoi applicare le idee di Hickey senza cambiare linguaggi o riscrivere tutto:
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.
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.
Inizia dove il cambiamento è già in corso:
Questo approccio migliora affidabilità e coordinazione del team mantenendo il sistema in funzione—e mantiene lo scope abbastanza piccolo da poter essere completato.
È 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”:
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”.
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.
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.
Anche in uno stile per lo più funzionale, i team hanno comunque bisogno di proprietà chiare:
Qui l’inclinazione di Clojure verso i dati e i confini espliciti aiuta, ma la disciplina è architetturale, non legata al linguaggio.
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.
Se il tuo sistema è già in produzione, non trattare queste idee come un rewrite totale. Cerca la modifica minima che riduce il rischio:
L’obiettivo non è la purezza—è meno sorprese a ogni cambiamento.
Questa è una checklist da sprint che puoi applicare senza cambiare linguaggi, framework o struttura del team.
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.
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.
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.
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à?”
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.
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?
L'immutabilità significa che non modifichi un valore in loco: crei un nuovo valore che rappresenta l'aggiornamento.
Praticamente, aiuta perché:
Non sempre è sbagliata. La mutabilità può essere una buona scelta quando è contenuta:
La regola chiave: non lasciare che le strutture mutabili trapelino oltre i confini dove molte parti possono leggere/scrivere.
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.
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:
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é:
Scegli un workflow che cambia spesso e applica tre passi:
Misura il successo con meno bug flakiness, minore raggio d’azione per ogni cambiamento e meno “coordinazione attenta” nei rilasci.