Esplora le idee di John Ousterhout su progettazione software pratica, l'eredità di Tcl, il confronto con Brooks e come la complessità affonda i prodotti.

John Ousterhout è uno scienziato informatico e ingegnere il cui lavoro attraversa sia la ricerca sia sistemi reali. Ha creato il linguaggio Tcl, ha contribuito a plasmare file system moderni e in seguito ha distillato decenni di esperienza in un'affermazione semplice, leggermente scomoda: la complessità è il nemico principale del software.
Questo messaggio è ancora attuale perché la maggior parte dei team non fallisce per mancanza di funzionalità o impegno—fallisce perché i loro sistemi (e le organizzazioni) diventano difficili da capire, difficili da modificare e facili da rompere. La complessità non rallenta solo gli ingegneri. Si infiltra nelle decisioni di prodotto, nella certezza della roadmap, nella fiducia dei clienti, nella frequenza degli incidenti e persino nel reclutamento—perché l'onboarding diventa un'odissea di mesi.
L'inquadramento di Ousterhout è pratico: quando un sistema accumula casi speciali, eccezioni, dipendenze nascoste e "sistemi fatti così una volta", il costo non si limita al codice. L'intero prodotto diventa più costoso da far evolvere. Le funzionalità richiedono più tempo, il QA diventa più difficile, i rilasci più rischiosi e i team iniziano a evitare miglioramenti perché toccare qualsiasi cosa sembra pericoloso.
Questo non è un appello alla purezza accademica. È un promemoria che ogni scorciatoia ha pagamenti di interessi—e la complessità è il debito a tasso più alto.
Per rendere l'idea concreta (e non solo motivazionale), guarderemo al messaggio di Ousterhout attraverso tre angoli:
Questo non è scritto solo per gli appassionati di linguaggi. Se costruisci prodotti, guidi team o prendi compromessi di roadmap, troverai modi azionabili per individuare la complessità presto, impedirne l'istituzionalizzazione e trattare la semplicità come un vincolo di primo piano—non come un optional dopo il lancio.
La complessità non è “tanto codice” o “matematica difficile”. È il divario tra quello che pensi che il sistema farà quando lo cambi e quello che fa davvero. Un sistema è complesso quando piccole modifiche sembrano rischiose—perché non puoi prevedere il raggio d'azione.
Nel codice sano, puoi rispondere: “Se cambiamo questo, cos'altro potrebbe rompersi?” La complessità è ciò che rende quella domanda costosa.
Si nasconde spesso in:
I team percepiscono la complessità come ritardo nelle consegne (più tempo speso a investigare), più bug (perché il comportamento è sorprendente) e sistemi fragili (le modifiche richiedono coordinamento tra molte persone e servizi). Mette anche sotto sforzo l'onboarding: i nuovi arrivati non riescono a costruire un modello mentale, quindi evitano i flussi core.
Parte della complessità è essenziale: regole di business, requisiti di conformità, casi limite del mondo reale. Non puoi cancellarla.
Ma molta è accidentale: API confuse, logica duplicata, flag “temporanei” che diventano permanenti e moduli che perdono dettagli interni. Questa è la complessità che le scelte di design creano—e l'unica che puoi ridurre con costanza.
Tcl è nato con un obiettivo pratico: rendere facile automatizzare software e estendere applicazioni esistenti senza riscriverle. John Ousterhout lo ha progettato affinché i team potessero aggiungere “la programmabilità necessaria” a uno strumento—poi consegnare quel potere a utenti, operatori, QA o chiunque dovesse scriptingare workflow.
Tcl ha reso popolare la nozione di linguaggio collante: uno strato di scripting piccolo e flessibile che connette componenti scritti in linguaggi più veloci e a basso livello. Invece di costruire ogni funzionalità in un monolite, potevi esporre un set di comandi e poi comporli in nuovi comportamenti.
Quel modello si è dimostrato influente perché corrispondeva a come il lavoro avviene davvero. Le persone non costruiscono solo prodotti; costruiscono sistemi di build, harness di test, strumenti admin, convertitori di dati e automazioni una tantum. Uno strato di scripting leggero trasforma quei compiti da “apri un ticket” a “scrivi uno script”.
Tcl ha reso l'embed una preoccupazione di primo piano. Potevi inserire un interprete in un'applicazione, esportare un'interfaccia di comandi pulita e ottenere immediatamente configurabilità e iterazione rapida.
Lo stesso schema riappare oggi in sistemi di plugin, linguaggi di configurazione, API di estensione e runtime di scripting embedded—sia che la sintassi sembri Tcl o no.
Ha anche rinforzato un'abitudine di design importante: separare primitive stabili (le capacità core dell'app host) dalla composizione modificabile (gli script). Quando funziona, gli strumenti evolvono più in fretta senza destabilizzare continuamente il core.
La sintassi di Tcl e il modello “tutto è una stringa” potevano risultare controintuitivi, e grandi codebase Tcl diventavano a volte difficili da comprendere senza convenzioni forti. Con l'arrivo di ecosistemi più nuovi con librerie standard più ricche, tooling migliore e community più grandi, molti team sono passati altrove.
Niente di ciò cancella l'eredità di Tcl: ha normalizzato l'idea che estendibilità e automazione non sono extra—sono funzionalità di prodotto che possono ridurre drasticamente la complessità per chi usa e mantiene il sistema.
Tcl è stato costruito attorno a un'idea apparentemente rigorosa: tenere il core piccolo, rendere la composizione potente e mantenere gli script leggibili abbastanza perché le persone possano collaborare senza traduzioni continue.
Invece di fornire un enorme set di funzionalità specializzate, Tcl puntava su un compatto set di primitive (stringhe, comandi, semplici regole di valutazione) e si aspettava che gli utenti le combinassero.
Quella filosofia spinge i progettisti verso meno concetti, riutilizzati in molti contesti. La lezione per prodotto e progettazione API è semplice: se puoi risolvere dieci bisogni con due o tre building block coerenti, riduci la superficie che le persone devono imparare.
Una trappola è ottimizzare per la comodità di chi costruisce. Una funzione può essere facile da implementare (copia un'opzione esistente, aggiungi un flag speciale, patcha un caso limite) ma rendere il prodotto più difficile da usare.
L'enfasi di Tcl era l'opposto: mantenere il modello mentale stretto, anche se l'implementazione deve fare più lavoro dietro le quinte.
Quando valuti una proposta, chiedi: questo riduce il numero di concetti che l'utente deve ricordare, o ne aggiunge uno in più?
Il minimalismo aiuta solo quando le primitive sono coerenti. Se due comandi sembrano simili ma si comportano diversamente nei casi limite, gli utenti finiranno a memorizzare trivia. Un piccolo set di strumenti può diventare “spigoli vivi” quando le regole variano sottilmente.
Pensa a una cucina: un buon coltello, una padella e un forno ti permettono di preparare molti piatti combinando tecniche. Un gadget che affetta solo avocado è un feature monouso: facile da vendere, ma ingombra i cassetti.
La filosofia di Tcl spinge per coltello e padella: strumenti generali che si compongono bene, così non serve un nuovo gadget per ogni ricetta.
Nel 1986 Fred Brooks scrisse un saggio con una conclusione intenzionalmente provocatoria: non esiste una singola svolta—nessuna “silver bullet”—che renderà lo sviluppo software un ordine di grandezza più veloce, economico e affidabile in un solo colpo.
Il punto non era che il progresso sia impossibile. Era che il software è già un medium dove possiamo fare quasi tutto, e quella libertà porta un onere unico: stiamo definendo continuamente la cosa mentre la costruiamo. Strumenti migliori aiutano, ma non cancellano la parte più dura del lavoro.
Brooks divide la complessità in due categorie:
Gli strumenti possono schiacciare la complessità accidentale. Pensate ai guadagni ottenuti da linguaggi di alto livello, controllo versione, CI, container, database gestiti e buoni IDE. Ma Brooks sosteneva che la complessità essenziale domina, e non scompare solo perché il tooling migliora.
Anche con le piattaforme moderne, i team spendono ancora la maggior parte dell'energia a negoziare requisiti, integrare sistemi, gestire eccezioni e mantenere comportamento coerente nel tempo. La superficie può cambiare (API cloud invece di driver di dispositivo), ma la sfida fondamentale resta: tradurre bisogni umani in comportamenti precisi e manutenibili.
Questo crea la tensione su cui si concentra Ousterhout: se la complessità essenziale non si può eliminare, può un design disciplinato ridurre in modo significativo quanto di essa trapela nel codice—e nelle teste degli sviluppatori giorno dopo giorno?
La gente a volte dipinge “Ousterhout vs Brooks” come una lotta tra ottimismo e realismo. È più utile leggerla come due ingegneri esperti che descrivono parti diverse dello stesso problema.
Brooks dice che non esiste una singola svolta che elimini la parte dura del software. Ousterhout non lo contesta davvero.
La sua obiezione è più circoscritta e pratica: i team spesso trattano la complessità come inevitabile quando molta di essa è auto-inflitta.
Nella visione di Ousterhout, un buon design può ridurre la complessità in modo significativo—non rendendo il software “facile”, ma rendendolo meno confuso da cambiare. È una grande affermazione, e conta perché la confusione è ciò che trasforma il lavoro quotidiano in lavoro lento.
Brooks punta sulla difficoltà essenziale: il software deve modellare realtà disordinate, requisiti che cambiano e casi limite esterni al codice. Anche con ottimi strumenti e persone brillanti, non puoi cancellarlo. Puoi solo gestirlo.
Le sovrapposizioni sono maggiori di quanto suggerisca il dibattito:
Invece di chiedersi “Chi ha ragione?”, chiediti: Quale complessità possiamo controllare questo trimestre?
I team non possono controllare i cambiamenti di mercato o la difficoltà intrinseca del dominio. Ma possono decidere se le nuove feature aggiungono casi speciali, se le API obbligano i chiamanti a ricordare regole nascoste e se i moduli nascondono o perdono complessità.
Questo è il terreno d'azione: accetta la complessità essenziale e sii implacabile nel limitare quella accidentale.
Un deep module è un componente che fa molto, esponendo però un'interfaccia piccola e facile da capire. La “profondità” è quanto complesso il modulo si prende carico: i chiamanti non devono conoscere i dettagli sgradevoli e l'interfaccia non li forza a farlo.
Un shallow module è l'opposto: può avvolgere una piccola logica, ma spinge la complessità all'esterno—con molti parametri, flag speciali, ordine di chiamata richiesto o regole “devi ricordare di…”.
Pensa a un ristorante. Un deep module è la cucina: ordini “pasta” da un menu semplice e non ti interessa chi fornisce gli ingredienti, i tempi di cottura o l'impiattamento.
Un shallow module è una “cucina” che ti dà ingredienti crudi con una scheda di 12 passi e ti chiede di portare la tua padella. Il lavoro si fa lo stesso—ma è stato trasferito al cliente.
Gli strati in più possono essere utili se collassano molte decisioni in una scelta ovvia.
Per esempio, un layer di storage che espone save(order) e gestisce internamente retry, serializzazione e indicizzazione è profondo.
Gli strati fanno danno quando rinominano soprattutto le cose o aggiungono opzioni. Se una nuova astrazione introduce più configurazione di quanta ne tolga—per esempio save(order, format, retries, timeout, mode, legacyMode)—è probabilmente superficiale. Il codice può sembrare “organizzato”, ma il carico cognitivo si vede in ogni punto di chiamata.
useCache, skipValidation, force, legacy.I deep module non solo “incapsulano codice”. Incapsulano decisioni.
Un'API “buona” non è solo quella che può fare molto. È quella che le persone possono tenere in testa mentre lavorano.
La lente di design di Ousterhout ti spinge a giudicare un'API in base allo sforzo mentale che richiede: quante regole devi ricordare, quante eccezioni prevedere e quanto è facile sbagliare.
Le API human-friendly tendono a essere piccole, coerenti e difficili da usare male.
Piccole non significa poco potenti—significa che la superficie è concentrata in pochi concetti che si compongono bene. Coerenti significa che lo stesso pattern funziona in tutto il sistema (parametri, gestione degli errori, naming, tipi di ritorno). Difficili da usare male significa che l'API guida verso percorsi sicuri: invarianti chiare, validazione ai confini e controlli che falliscono presto.
Ogni flag, modalità o configurazione “per ogni evenienza” diventa una tassa su tutti gli utenti. Anche se solo il 5% dei chiamanti ne ha bisogno, il 100% dei chiamanti deve comunque sapere che esiste, chiedersi se serve e interpretare il comportamento quando interagisce con altre opzioni.
Così le API accumulano complessità nascosta: non in una singola chiamata, ma nella combinatoria.
I default sono una gentilezza: permettono alla maggior parte dei chiamanti di omettere decisioni e ottenere comunque comportamento sensato. Le convenzioni (un modo ovvio per farlo) riducono i rami nella mente dell'utente. Il naming fa un lavoro reale: scegli verbi e sostantivi che rispecchiano l'intento e mantieni operazioni simili con nomi simili.
Un promemoria: le API interne contano tanto quanto quelle pubbliche. La maggior parte della complessità nei prodotti vive dietro le quinte—confini di servizio, librerie condivise e moduli “helper”. Tratta quelle interfacce come prodotti, con review e disciplina di versioning.
La complessità raramente arriva come una singola “brutta decisione”. Si accumula attraverso piccole patch sensate—soprattutto quando i team sono sotto pressione e l'obiettivo immediato è consegnare.
Una trappola è feature flag ovunque. I flag servono per rollout sicuri, ma quando restano, ogni flag moltiplica i comportamenti possibili. Gli ingegneri smettono di ragionare sul “sistema” e iniziano a ragionare sul “sistema, tranne quando il flag A è attivo e l'utente è nel segmento B”.
Un'altra è la logica per casi speciali: “i clienti enterprise necessitano X”, “eccetto nella regione Y”, “a meno che l'account non abbia più di 90 giorni”. Queste eccezioni spesso si diffondono nel codice e dopo qualche mese nessuno sa più quali siano ancora necessarie.
Una terza è astrazioni che perdono. Un'API che costringe i chiamanti a capire dettagli interni (timing, formato di storage, regole di caching) spinge la complessità all'esterno. Invece di un modulo che si prende l'onere, ogni chiamante impara le stranezze.
Programmazione tattica ottimizza per questa settimana: fix rapidi, cambi minimi, “patcha e vai”.
Programmazione strategica ottimizza per l'anno prossimo: piccoli riprogettamenti che prevengono la stessa classe di bug e riducono il lavoro futuro.
Il pericolo è l’“interesse di manutenzione”. Una soluzione rapida sembra economica ora, ma la paghi con interessi: onboarding più lento, rilasci fragili e uno sviluppo guidato dalla paura in cui nessuno vuole toccare il codice vecchio.
Aggiungi prompt leggeri alla code review: “Questo aggiunge un nuovo caso speciale?” “L'API può nascondere questo dettaglio?” “Quale complessità stiamo lasciando dietro di noi?”
Tieni piccoli registri di decisione per i tradeoff non banali (qualche punto è sufficiente). E riserva un piccolo budget di refactor in ogni sprint così le correzioni strategiche non siano considerate lavoro extra.
La complessità non resta intrappolata nell'ingegneria. Si perde nei tempi, nell'affidabilità e nell'esperienza che i clienti vivono del prodotto.
Quando un sistema è difficile da comprendere, ogni cambiamento richiede più tempo. Il time-to-market slitta perché ogni rilascio richiede più coordinazione, più test di regressione e più cicli “per sicurezza”.
L'affidabilità soffre. I sistemi complessi generano interazioni che nessuno può prevedere completamente, quindi i bug emergono come casi limite: il checkout fallisce solo quando coupon, carrello salvato e regola fiscale regionale si combinano in un certo modo. Questi sono gli incidenti più difficili da riprodurre e più lenti da risolvere.
L'onboarding diventa un freno nascosto. I nuovi non riescono a costruire un modello mentale utile, evitano le aree rischiose, copiano pattern che non comprendono e aggiungono involontariamente altra complessità.
I clienti non si preoccupano se un comportamento è causato da un “caso speciale” nel codice. Lo vivono come incoerenza: impostazioni che non valgono ovunque, flussi che cambiano a seconda di come ci arrivi, feature che funzionano “la maggior parte delle volte”.
La fiducia cala, il churn aumenta e l'adozione si arresta.
I team di support pagano la complessità con ticket più lunghi e più scambi per raccogliere contesto. Operations paga con più alert, più runbook e deployment più cauti. Ogni eccezione diventa qualcosa da monitorare, documentare e spiegare.
Immagina la richiesta di “un'altra regola di notifica”. Aggiungerla sembra veloce, ma introduce un nuovo ramo di comportamento, più testo nell'UI, più casi di test e più modi in cui gli utenti possono configurarsi male.
Ora confrontalo con semplificare il flusso di notifiche esistente: meno tipi di regole, default più chiari e comportamento coerente su web e mobile. Potresti rilasciare meno manopole, ma riduci le sorprese—rendendo il prodotto più facile da usare, più facile da supportare e più veloce da far evolvere.
Tratta la complessità come performance o sicurezza: pianificala, misurala e proteggila. Se te ne accorgi solo quando la delivery rallenta, stai già pagando interessi.
Insieme allo scope delle feature, definisci quanto nuova complessità una release può introdurre. Il budget può essere semplice: “niente concetti netti nuovi a meno che non ne rimuoviamo uno” o “qualsiasi nuova integrazione deve sostituire un percorso vecchio”.
Rendi espliciti i tradeoff nella pianificazione: se una feature richiede tre nuove modalità di configurazione e due eccezioni, dovrebbe “costare” più di una che si inserisce nei concetti esistenti.
Non servono numeri perfetti—solo segnali che mostrino una tendenza:
Monitora queste per release e collega le decisioni: “Abbiamo aggiunto due nuove opzioni pubbliche; cosa abbiamo rimosso o semplificato per compensare?”
I prototipi sono spesso giudicati su “Riusciamo a costruirlo?” Invece, usali per rispondere: “Questo sembra semplice da usare e difficile da usare male?”
Fai svolgere a qualcuno che non conosce la feature un compito realistico con il prototipo. misura il tempo per riuscirci, le domande poste e dove sbagliano assunzioni. Questi sono i punti caldi della complessità.
Qui entra anche il valore dei moderni workflow di build—se mantengono l'iterazione stretta e permettono un facile rollback. Per esempio, quando i team usano una piattaforma di vibe-coding come Koder.ai per abbozzare uno strumento interno o un nuovo flusso via chat, funzionalità come planning mode (per chiarire l'intento prima della generazione) e snapshots/rollback (per annullare cambi rischiosi rapidamente) possono rendere la sperimentazione iniziale più sicura—senza impegnarsi in una pila di astrazioni a metà. Se il prototipo passa, puoi esportare il codice sorgente e applicare la stessa disciplina di “deep module” e progettazione API descritta sopra.
Rendi il lavoro di “cleanup della complessità” periodico (ogni trimestre o a ogni major release) e definisci cosa significa “finito”:
L'obiettivo non è codice più pulito in astratto—ma meno concetti, meno eccezioni e cambiamenti più sicuri.
Ecco alcune mosse che traducono l'idea di Ousterhout “la complessità è il nemico” in abitudini settimanali del team.
Scegli un sottosistema che crea spesso confusione (onboarding doloroso, bug ricorrenti, molte domande “come funziona?”).
Seguiti interni che puoi avviare: una “complexity review” nella pianificazione (testo visibile: /blog/complexity-review) e un controllo rapido per verificare se il tooling sta riducendo la complessità accidentale invece di aggiungere strati (testo visibile: /pricing).
Quale pezzo di complessità rimuoveresti per primo se potessi cancellare un solo caso speciale questa settimana?
La complessità è il divario tra quello che ti aspetti che succeda quando modifichi il sistema e quello che succede davvero.
La senti quando piccole modifiche sembrano rischiose perché non riesci a prevedere l'area di impatto (test, servizi, configurazioni, clienti o casi limite che potresti rompere).
Cerca segnali che il ragionamento è costoso:
La complessità essenziale deriva dal dominio (regole, normative, casi limite del mondo reale). Non la puoi eliminare—puoi solo modellarla bene.
La complessità accidentale è auto-inflitta (astrazioni che perdono, logica duplicata, troppi modi/flag, API poco chiare). Questa è la parte che i team possono ridurre con progetto e semplificazione.
Un deep module fa molto ma espone un'interfaccia piccola e stabile. Assorbe i dettagli sporchi (retry, formati, ordine, invarianti) così che i chiamanti non debbano occuparsene.
Prova pratica: se la maggior parte dei chiamanti può usare il modulo correttamente senza conoscere le regole interne, è profondo; se devono memorizzare sequenze e regole, è superficiale.
Sintomi comuni:
legacy, skipValidation, force, mode).Preferisci API che siano:
Prima di aggiungere “solo un'altra opzione”, chiediti se puoi riprogettare l'interfaccia in modo che la maggior parte dei chiamanti non debba pensarci.
Usa i feature flag per rollout controllati, poi trattali come debito con una data di fine:
I flag che restano a lungo moltiplicano il numero di “sistemi” che gli ingegneri devono ragionare.
Rendi esplicita la complessità in fase di pianificazione, non solo in code review:
L'obiettivo è portare i compromessi in vista prima che la complessità si istituzionalizzi.
Programmazione tattica ottimizza per questa settimana: patch rapide, cambi minimi, “ship it”.
Programmazione strategica ottimizza per il prossimo anno: piccoli riprogettamenti che rimuovono classi ricorrenti di bug e riducono il lavoro futuro.
Una regola pratica: se una correzione richiede conoscenza del chiamante (“ricorda di chiamare X prima” o “imposta questo flag solo in prod”), probabilmente serve una modifica strategica per nascondere quella complessità dentro il modulo.
La lezione duratura di Tcl è il potere di un piccolo set di primitive più una forte composizione—spesso come livello embedded di “collante”.
Equivalenti moderni:
L'obiettivo di design è lo stesso: mantenere il core semplice e stabile e lasciare il cambiamento alle interfacce pulite.
I moduli superficiali sembrano organizzati ma spostano la complessità su ogni chiamante.