Se di questo corso non ti ricordi nient’altro, ricordati questa lezione. È l’unica frase attorno a cui orbitano le altre 80.
Ogni decisione architetturale è un trade-off. Non esiste un’architettura “migliore”, esiste solo quella “che si adatta meglio a questo set di vincoli.” Un sistema veloce sui dati piccoli di solito è lento sui dati grandi. Un sistema che scala orizzontalmente fino a mille macchine di solito ci riesce rinunciando a qualcos’altro, spesso la consistency, spesso la semplicità operativa. Un sistema che è banale da gestire di solito è uno che ancora non deve fare granché. Nel momento in cui gli chiedi di fare di più, la semplicità inizia a consumarsi.
Quando uno stakeholder entra in una stanza e dice “vogliamo un sistema veloce, scalabile, consistent, economico, facile da gestire e facile da estendere”, ciò che sta dicendo davvero è che ancora non sa a quale di queste cose tiene di più. Il tuo lavoro, come architetto, è tradurre “vogliamo tutto” in “ecco su cosa saremo eccellenti, ecco su cosa saremo solo adeguati, ed ecco cosa sacrificheremo deliberatamente.” Quest’ultima lista è quella che nessuno ha voglia di scrivere. Scriverla è il lavoro.
Questa lezione è l’indice dei trade-off con un nome che vengono fuori più spesso nei sistemi reali. Non andiamo in profondità su nessuno di loro per ora. Il resto del corso è, in gran parte, un’immersione profonda nei singoli trade-off e nel modo in cui i sistemi reali li navigano. Oggi è la mappa.
La tesi
Ogni qualità di un sistema ha un costo in un’altra qualità. Non esistono pasti gratis nei sistemi distribuiti, nei database, nei formati di storage, o nei modelli di deployment. La cosa più vicina al “ce l’abbiamo tutto” che potrai mai vedere è “siamo abbastanza bravi sulla maggior parte di esse, e male sulle poche che si rivelano non importanti.” Riconoscere in anticipo quali qualità non contano, per questo sistema, con questa user base, su questo budget, è la maggior parte dell’architettura.
La frase “abbastanza bravi sulla maggior parte, scarsi su poche” non è disfattismo. È la verità strutturale di chi costruisce qualcosa che esiste nel mondo reale. Una macchina che va a 300 km/h non è anche una berlina familiare a basso consumo. Un database che scala linearmente fino a 1.000 nodi non è anche la cosa più facile da debuggare alle 3 di notte. Un’app Rails monolitica che puoi deployare in 90 secondi non è anche il substrato su cui farai girare un’organizzazione da 200 ingegneri. Ognuno di questi è un buon prodotto. Ognuno di loro è buono perché qualcuno ha deciso cosa contava.
I trade-off con un nome
Quel che segue è il catalogo. Non è esaustivo: ho lasciato fuori i piccoli. Questi sono i sette che vengono fuori in quasi ogni conversazione di architettura reale che ho avuto.
1. Latency contro throughput
La latency è quanto tempo impiega una singola richiesta. Il throughput è quante richieste al secondo gestisce il sistema. Non sono la stessa cosa, e ottimizzare l’una spesso danneggia l’altra.
Un sistema raggruppato in batch da 1.000 richieste, processati ogni 100 ms, ha un throughput eccellente (10.000 richieste al secondo) e una latency pessima (100 ms minimo, spesso di più). Un sistema che processa ogni richiesta non appena arriva ha una latency eccellente (sotto il millisecondo, a volte) e un throughput peggiore, perché il batching ammortizza l’overhead e il lavoro non-batched non se lo può permettere.
In un analytics warehouse di solito ottimizzi il throughput; l’utente che lancia un GROUP BY country su 4 TB non si interessa se ci mette 8 secondi o 12, ma si interessa che il cluster riesca a servire 50 query del genere in concorrenza. In un sistema di payment authorization vince la latency; il terminale del merchant ha bisogno di una risposta sotto i 200 ms, ogni volta, anche se vuol dire che il throughput per nodo è una frazione di quello che potrebbe essere.
La trappola è trattare “veloce” come una qualità singola. Non lo è. Ogni volta che qualcuno dice di volere un sistema “veloce”, la tua domanda di follow-up è: veloce a fare cosa, per chi, sotto quale carico? La risposta raramente comprende entrambi gli assi.
2. Consistency contro availability
Questo è il trade-off del teorema CAP, e ha una lezione tutta sua (lezione 10), ma il titolo è: in un sistema distribuito, quando la rete si partiziona, devi scegliere. Puoi rifiutarti di servire le richieste finché la partition non guarisce (consistency, al costo della availability) oppure puoi servire dati potenzialmente stantii su entrambi i lati finché non si riconciliano (availability, al costo della consistency).
Il servizio di account-balance della tua banca sceglie consistency. Preferisce mostrarti una pagina di errore durante una network partition piuttosto che dirti che hai 100 euro su un conto che in realtà è scoperto da dieci minuti. Il contatore dei like del tuo social network sceglie availability. Preferisce mostrarti un numero leggermente stantio, o anche un numero che brevemente cala prima di salire, piuttosto che rifiutarsi di caricare la pagina del tutto.
Nessuna delle due risposte è “giusta”. Sono prodotti diversi con tolleranze diverse rispetto allo sbagliare. La conversazione DDB-contro-Spanner-contro-Postgres è, in fondo, questo trade-off in tre forme diverse. Ci passeremo molto tempo.
3. Read-optimized contro write-optimized
La forma del tuo storage engine determina in cosa è veloce. I motori basati su B-tree (Postgres, MySQL con InnoDB, SQL Server) sono eccellenti su point reads e range scan su colonne indicizzate; il loro write path è più costoso perché ogni insert può aver bisogno di rebilanciare le pagine sul disco. I motori basati su LSM-tree (RocksDB, Cassandra, BigTable, ScyllaDB, parti di Mongo) sono eccellenti sulle scritture ad alto volume; il loro read path è più costoso perché una key può vivere in una qualunque di parecchie SSTable a strati e il motore deve fondere i risultati.
Se stai costruendo un sistema che principalmente legge (un CMS, una dashboard interna, una tipica web app), scegli un database B-tree. Se stai costruendo un sistema che principalmente scrive (un backend di metriche, un event log, uno storage di chat history), scegli un database LSM. Se stai costruendo un sistema che fa entrambe le cose, scegli quello il cui lato debole sai compensare con caching, batching, o uno store di lettura separato.
Questo trade-off ha lavorato sul system design per trent’anni e non dà segni di sparire. Ci torneremo nel modulo 4 (data systems).
4. Semplice contro flessibile
Un file YAML di configurazione con due settings è più semplice di un sistema a plugin. Un sistema a plugin è più flessibile. Entrambi hanno il loro momento. Scegliere quello sbagliato per il momento in cui ti trovi è uno degli errori architetturali più comuni.
All’inizio della vita di un progetto, vince il semplice. I due casi che hai oggi sono i due casi che hai oggi, e una config che li gestisce in 30 righe è più veloce da scrivere, più veloce da leggere, più veloce da debuggare e più veloce da cancellare quando i requisiti cambiano. La flessibilità che avresti costruito in un sistema a plugin sta pagando interessi su un prestito che non hai mai chiesto.
Più avanti, quando hai 50 casi invece di due, il semplice perde. La config da 30 righe è diventata una config da 3.000 righe; ogni voce è un caso particolare; nessuno sa prevedere quale modifica romperà cosa. Allora il sistema a plugin che non hai costruito cinque anni fa inizia a sembrare un affare.
L’abilità sta nel notare da quale lato del punto di flesso ti trovi. Il default onesto è: costruisci semplice, guardalo crescere, refactora a flessibile quando i casi smettono di poter essere aggiunti in modo pulito, non quando sono semplicemente numerosi. La “flessibilità prematura” è costata più progetti della “flessibilità mancante.”
5. Decentralizzato contro coordinato
La scala vuole decentralizzazione. La correttezza vuole coordinazione. Queste due tirano in direzioni opposte su quasi ogni questione di sistemi distribuiti.
Una cache globalmente distribuita che lascia decidere a ogni nodo cosa fare evict è decentralizzata; scala linearmente e sopravvive alla morte di qualunque singolo nodo, ma due nodi possono tenere uno stato inconsistente per un po’. Una cache che usa un coordinatore centrale per assegnare le chiavi ai nodi è coordinata; lo stato è sempre consistent ma il coordinatore è ora un collo di bottiglia e un single point of failure.
Il DNS è decentralizzato. Scala all’intero pianeta perché nessun nodo deve coordinarsi con un altro. Il costo è che la propagazione richiede da minuti a ore; non puoi usare il DNS come meccanismo di consistency in tempo reale. Un database relazionale tradizionale è coordinato. Ogni scrittura passa per un singolo primary, ed è per questo che ACID è semplice e che scalare le scritture oltre una macchina è difficile.
La maggior parte dei sistemi reali sceglie un punto intermedio: coordinato dentro un piccolo blast radius (un servizio, una region, una partition) e decentralizzato tra blast radii diversi. Sapere quale asse vale quale costo di coordinazione è metà del system design.
6. Costo del build contro costo del buy
Molte decisioni architetturali sono travestite da domande tecniche ma in realtà sono domande di pricing. “Dovremmo usare Kafka o Kinesis?” raramente riguarda i meriti ingegneristici dei due sistemi. Riguarda se preferisci pagare ad AWS più o meno 0,08 dollari per GB per un prodotto managed senza lavoro operativo, o pagare i tuoi ingegneri per gestire un cluster Kafka che costa di meno per GB ma si mangia la rotazione di on-call di due ingegneri.
La stessa domanda ricorre dappertutto: Postgres managed contro self-hosted, Auth0 contro un servizio di auth fatto in casa, Stripe contro un proprio payment processor, Snowflake contro una lakehouse self-hosted Spark+Iceberg. La risposta cambia quasi sempre con la dimensione del team e con lo stadio di crescita. Una startup di due persone che compra tutto dai vendor e spedisce prodotto sta facendo la chiamata giusta. Un’azienda da 200 ingegneri che paga 4 milioni di dollari l’anno per Auth0 ha probabilmente attraversato la linea oltre la quale costruirselo sarebbe stato più economico. Un’azienda da 2.000 ingegneri che si costruisce ogni cosa è di nuovo dal lato sbagliato.
La trappola è che la risposta non è stabile. La chiamata giusta a 5 ingegneri è sbagliata a 50, e la chiamata giusta a 50 è sbagliata a 500. L’architettura deve rivisitare il costo-del-build contro il costo-del-buy su ogni pezzo importante, ogni paio d’anni. La maggior parte dei team non lo fa, e finisce per pagare otto cifre l’anno per qualcosa che ora potrebbe far girare in casa, o per affondare un team nel mantenere un sistema fatto in casa che ora è strettamente peggiore della SaaS che potrebbero comprare.
7. Time to market contro costo a lungo termine
L’ultimo trade-off, e probabilmente il meta-trade-off che contiene tutti gli altri. Ogni scorciatoia che prendi oggi ha un costo futuro. Ogni investimento che fai in un’architettura pulita di lungo periodo ha un costo oggi.
Se sei in una startup con 18 mesi di runway, la chiamata giusta è quasi sempre spedire la versione tenuta su con il nastro adesivo. Valori hardcoded, niente test sui percorsi noiosi, niente documentazione, niente astrazioni, tutte le feature dietro feature flag così le puoi strappare via a basso costo. Stai correndo contro il runway, e la maggior parte del nastro adesivo verrà sostituita comunque perché il prodotto che spedisci quasi certamente non è il prodotto che il mercato vuole. Investire in un’architettura pulita per una feature che verrà tagliata in tre mesi è la forma di lavoro più costosa.
Se sei in un’azienda di 10 anni con un revenue solido, la chiamata giusta si inverte. Il nastro adesivo che scrivi oggi sarà ancora lì nel 2034, sarà gestito da persone che ancora non sono entrate in azienda, e accumulerà interessi composti in costo di manutenzione. Spendere una settimana in più per farlo bene è ripagare un debito da 40 settimane che qualcun altro avrebbe ereditato.
Gli istinti della maggior parte degli ingegneri sono calibrati su un estremo di questo trade-off, in base a dove hanno passato gli anni formativi. L’abilità sta nel notare in quale ambiente ti trovi adesso e regolarti di conseguenza. Un ingegnere da startup che scrive architettura Google-scale nella prima settimana non spedisce nulla. Un ingegnere d’enterprise che tiene su con il nastro adesivo il flow della carta di credito spedisce un incidente regolatorio.
La 2x2
Ecco un modo per visualizzare quattro di questi trade-off come quadranti con un nome:
flowchart TD
subgraph Q1["High consistency, high availability"]
A1["Hard. Spanner, FoundationDB.<br/>Achievable with cost and complexity."]
end
subgraph Q2["High consistency, low availability"]
A2["Traditional RDBMS in single primary.<br/>Postgres, classic SQL Server."]
end
subgraph Q3["Low consistency, high availability"]
A3["Eventual consistency.<br/>DynamoDB default, Cassandra, DNS."]
end
subgraph Q4["Low consistency, low availability"]
A4["Nobody chooses this on purpose.<br/>It's where misconfigured systems land."]
end
Tre di quei quattro quadranti sono scelte architetturali reali per workload reali. Il quarto, low-consistency-low-availability, è il posto dove i sistemi finiscono quando nessuno ha fatto una scelta deliberata e i default puntavano tutti nella direzione sbagliata. Se il tuo post-mortem recita “abbiamo perso dati e il servizio era anche giù”, sei nel quadrante quattro, e il quadrante quattro è sempre un incidente.
”Vogliamo tutto”
La frase più costosa nella riunione di architettura è “vogliamo tutto.” Veloce, scalabile, consistent, economico, semplice, flessibile, e pronto entro Q2. I sistemi reali non esistono in questo punto dello spazio di design. Non possono. La cosa più vicina a cui l’industria sia arrivata è Google Spanner, che ti dà strong consistency geo-distribuita al costo di essere caro da far girare, caro da licenziare, di richiedere atomic clocks (sì, quelli veri), e di essere soggetto al pavimento di latency della replica WAN sincrona. Spanner è probabilmente il sistema più caro dell’industria, ed è quello che fa il minor numero di trade-off. Non è una coincidenza; è la struttura del trade-off.
Quando qualcuno al tavolo chiede tutto, il tuo lavoro non è discuterci. Il tuo lavoro è tradurre. “Se ottimizziamo per latency a p99 sotto i 50 ms, lo pagheremo in throughput per macchina e in availability regionale; ecco tre architetture e cosa ognuna sacrifica.” Quella frase non costa loro nulla se le priorità sono ancora flessibili, e costa loro settimane sulla data di delivery se non lo sono. In un modo o nell’altro hai informato la decisione, invece di trovarti sorpreso più tardi.
Gli architetti che si bruciano sono quelli che provano a consegnare “tutto” e si ritrovano sei mesi dopo con un sistema mediocre su ogni asse, perché i trade-off non sono mai stati fatti consapevolmente. Gli architetti che non si bruciano sono quelli che fanno i trade-off presto, li scrivono in un ADR (lezione 4), e li rivisitano ogni sei mesi. Il sistema mediocre è il costo inevitabile del rifiutarsi di scegliere. Il sistema solo-adeguato-su-quasi-tutto-ed-eccellente-sulle-cose-che-contano è quello che vince.
Cosa arriva
Il resto di questo corso è per lo più trade-off con un nome e come i sistemi reali li navigano. La lezione 6 ti accompagna nell’architettura più semplice possibile (una VM, un database) e mostra a cosa rinuncia per essere così semplice. Le lezioni dalla 9 alla 12 vanno in profondità su consistency contro availability e sulla famiglia CAP. Il modulo 4 è la sfida tra storage engine read-vs-write. Il modulo 6 è il bilancio buy-vs-build, simple-vs-flexible per code, cache, search, e ML serving. Il modulo 9 è la storia time-to-market contro costo-a-lungo-termine sul lato operativo: deployment, observability, on-call.
Ognuna di quelle lezioni parte dalle stesse fondamenta: questa cosa è un trade-off, ecco gli assi, ecco quale di loro questa tecnologia sceglie, ecco a cosa rinuncia, ed ecco quando sceglieresti l’altro.
L’architettura non è un set di pattern da memorizzare. È un set di trade-off da navigare. I pattern sono di second’ordine; sono soluzioni cristallizzate a configurazioni specifiche di trade-off che ricorrono abbastanza spesso da meritarsi un nome. Ne incontreremo parecchi. Arriveremo a ognuno di loro per via del trade-off che risolve.
Prossima lezione: l’architettura più semplice che puoi spedire. Una VM, un Postgres, un processo, e una carriera del tutto rispettabile costruita sopra quella forma.