Architettura di dati e sistemi, dalle fondamenta Lezione 6 / 80

La prima architettura: una web app single-server con database

Che aspetto ha un sistema funzionante alla scala da startup: una VM, un Postgres, un processo, e perché è un punto di partenza assolutamente rispettabile.

La lezione precedente parlava di trade-off. Questa parla dell’architettura che dovresti scegliere quando non ti sei ancora guadagnato nessuno dei trade-off più difficili. La maggior parte dei sistemi dovrebbe partire da qui. Un numero sorprendente di sistemi non dovrebbe mai andarsene.

La forma: una virtual machine, un database Postgres, un processo applicativo, una pipeline di deploy. Opzionalmente un frontend statico su una CDN, opzionalmente un load balancer gestito davanti, opzionalmente una piccola cache quando ne hai davvero bisogno. Questo è l’intero diagramma. Lo puoi disegnare sul retro di un tovagliolo. Ci puoi far girare un business profittevole. La gente lo fa, ogni giorno.

Questa lezione parla del perché questo è un buon punto di partenza, di cosa può e non può fare, di quanto costa, di quali sono i pezzi reali, e di cosa dovresti resistere ad aggiungerci sopra il più a lungo umanamente possibile.

Che aspetto ha la “minimal viable architecture”

L’architettura minima ha quattro parti in movimento:

  1. Un web framework, in qualunque linguaggio il tuo team sia più veloce. Python (Django, FastAPI, Flask), Node (Express, Fastify, NestJS), Go (net/http o chi o Gin), Ruby (Rails), Elixir (Phoenix), .NET (ASP.NET Core), Java (Spring Boot, o Quarkus se ti piace il cold start). Vanno tutti bene. La scelta a questo stadio conta a malapena. Prendi quello del cui ecosystem ti sai muovere alle 2 di notte.
  2. Un database relazionale. Postgres è la scelta sicura. MySQL/MariaDB è la seconda scelta sicura. SQLite è genuinamente fattibile per molti workload fino a diverse migliaia di utenti; non ridere. Evita qualunque cosa più alla moda; non ti sei guadagnato la complessità operativa.
  3. Un reverse proxy o un load balancer davanti. O nginx sulla stessa VM, o un load balancer gestito (AWS ALB, Cloudflare, Hetzner Load Balancer, DigitalOcean Load Balancer). Termina TLS, passa l’HTTP alla tua app.
  4. Un modo per fare deploy. Può essere sofisticato quanto vuoi o grezzo come git pull && systemctl restart app. Entrambi gli estremi hanno spedito business reali.

Questo è tutto lo stack. Tutto il resto (cache, queue, frontend separato, CDN, search, analytics warehouse) è qualcosa che si bullona sopra quando puoi nominare un problema specifico che ti risolve, non perché i diagrammi di architettura nei talk delle conference hanno quei box.

Ecco il diagramma C4 container per questo:

flowchart LR
    user[End user browser] -->|HTTPS| lb[Load balancer or nginx]
    lb -->|HTTP| app[Web app process]
    app -->|TCP 5432| db[(Postgres database)]
    cdn[CDN] -.->|static assets| user

Tutto qui. Un punto di ingresso user-facing, un processo applicativo, un database, opzionalmente una CDN per i file statici. L’intera cosa ci sta in testa, puoi fare debug di qualunque sua parte da un laptop, e puoi deployare una nuova versione in meno di cinque minuti.

Cosa può fare davvero questa cosa

L’istinto, specialmente nel 2026 dopo un decennio di marketing dei microservizi, è pensare che questa forma sia un giocattolo. Non lo è.

Una web app ragionevolmente tunata su una singola VM da 4 vCPU e 8 GB, che parla con un Postgres sulla stessa macchina o su un vicino gestito, può comodamente reggere:

  • Centinaia di richieste al secondo senza alcuno sforzo ingegneristico speciale. Postgres gestisce le point query in millisecondi a singola cifra; l’overhead del tuo framework è di decine di millisecondi al massimo; la rete davanti è il collo di bottiglia più spesso di quanto non lo sia l’app.
  • Migliaia di richieste al secondo con un po’ di cura. Quella cura ha questo aspetto: connection pooling (PgBouncer o il pool integrato del tuo framework, dimensionato correttamente), indici ragionevoli sulle colonne su cui filtrano le tue query, evitare le N+1 query usando JOIN o eager loading invece di una query per riga, e una cache per i path genuinamente caldi.
  • Decine di milioni di righe nelle tue tabelle più grandi prima di cominciare a doverti mettere a pensare seriamente al partitioning. Postgres è felice a questa scala. Gli indici B-tree sono felici. Le sequential scan sulla rara query scritta male sono ancora accettabili.
  • Decine di migliaia di utenti concorrenti se la tua applicazione si comporta bene e la maggior parte del tuo traffico è in lettura. Le connessioni WebSocket si distribuiscono per processo; se ti servono un milione di socket concorrenti hai un problema diverso, ma pochissime app ne hanno davvero bisogno.

Una piccola startup che processa 10.000 ordini al giorno, una SaaS con 5.000 clienti paganti, un content site con 500.000 lettori al mese, un tool B2B con 200 utenti enterprise che lo martellano in orario d’ufficio: tutti questi girano comodamente su questa forma, con margine in più. Le aziende che hanno bisogno di più di così sono reali, ma sono una frazione più piccola dell’industria di quanto il circuito delle conference suggerisca.

La verità non sentimentale è che il database fa quasi tutto il lavoro. Il processo applicativo è per lo più colla: accetta una richiesta HTTP, valida l’input, esegue qualche query, formatta l’output. Se la tua app è lenta, è quasi sempre perché una query è lenta, non perché il framework è lento. Saperlo ti salva dall’errore di buttare più application server contro un problema che è in realtà un indice mancante.

Quanto costa

Numeri reali, in EUR, al 2026, per “ho un side project / una piccola SaaS che deve girare da qualche parte”:

  • Hetzner Cloud CPX21: 4 vCPU, 8 GB RAM, 80 GB SSD, 20 TB di traffico. Circa EUR 13/month. Datacentre in Germania o Finlandia, latenza eccellente per gli utenti europei.
  • DigitalOcean Basic Droplet, 4 vCPU, 8 GB RAM, forma simile. Circa EUR 45/month. Più region globali, dashboard leggermente più piacevole.
  • AWS EC2 t4g.large (2 vCPU ARM, 8 GB) o t3.medium (2 vCPU, 4 GB). Circa EUR 25 to 40/month più data transfer, più snapshot, più la fattura che nessuno sa prevedere.
  • Postgres gestito, se vuoi evitare di gestirlo tu: Hetzner non ne offre uno per ora; il managed Postgres di DigitalOcean parte intorno a EUR 15/month per una piccola istanza; AWS RDS per un db.t4g.small è intorno a EUR 30/month, e uno vero da produzione con backup e replica si avvicina a EUR 80.
  • Cloudflare davanti per DNS, TLS, caching, protezione DDoS: gratis per la maggior parte dei piccoli workload. Il piano Pro è USD 25/month se vuoi WAF, analytics, image optimisation. La maggior parte dei piccoli progetti sta bene sul piano free.
  • Backup su object storage S3-compatible (Backblaze B2 o Hetzner Storage Box): pochi EUR/month per i volumi che produce una piccola SaaS.
  • Monitoring via il tier gratuito di Grafana Cloud, BetterStack, o Prometheus self-hosted sulla stessa VM: gratis, o pochi EUR a singola cifra per un’alternativa gestita.

Bolletta mensile totale per un deployment di produzione funzionante: da qualche parte tra EUR 30 e EUR 100, a seconda di se ti self-hosti il database e di quanto valuti il non doverlo gestire tu. È meno di quanto il tier di esplorazione di AWS ti fattura per sbaglio quando ti dimentichi di spegnere un NAT gateway. Un business reale, scalato, con clienti paganti, può girare su questo per molto tempo.

L’unit economics è estremamente amichevole con i piccoli team. Se hai 100 utenti paganti a EUR 20/month, hai EUR 2.000/month di ricavi e EUR 50/month di infrastruttura. Quasi qualunque altra forma architetturale costa di più, sia in compute sia in tempo di engineering. Il modo più economico di operare è non avere molte cose da operare.

Perché questo è il punto di partenza giusto

Tre ragioni, tutte sull’umano che gestisce il sistema, non sulla macchina.

Semplice da ragionarci sopra. Quando qualcosa si rompe, ci sono quattro posti in cui può essere: il load balancer, l’app, il database, o la connessione fra loro. SSH dentro la VM, guarda i log, guarda top, guarda pg_stat_activity. Il modello mentale è abbastanza piccolo da starci nella memoria di lavoro mentre fai debug. Confrontalo con un deployment di microservizi a 12 servizi dove la richiesta potrebbe essere in una qualunque di sette code asincrone e non puoi capire quale.

Veloce da deployare. Una nuova versione del codice su questo stack è git pull, esegui le migration, riavvia il processo. Cinque minuti da “l’ho fixato sul mio laptop” a “è live in produzione”. Questo si compone. Un team che spedisce dieci volte al giorno impara dieci volte più in fretta di un team che spedisce una volta a settimana, e a questo stadio di un progetto, la velocità di apprendimento è tutto il gioco.

Veloce da debuggare. Gli stack trace sono locali. I log sono in un posto solo. Il tempo dell’orologio sul muro coincide con il tempo nel database e con il tempo nell’applicazione. Non ci sono distributed trace da ricostruire, non c’è clock skew tra servizi, non ci sono message queue che nascondono l’ordine delle operazioni. Quando riproduci un bug, lo riproduci nello stesso modo ogni volta.

I solo founder e i piccoli team spediscono anni di prodotto su questa forma. Stripe ha girato su Ruby on Rails e un Postgres per molto tempo. Basecamp lo fa ancora, per scelta. La Nomad List di Pieter Levels gira su una singola VM con un singolo database SQLite, lo fa da anni, e stampa soldi. Plausible Analytics, prima di crescere, era una singola app Phoenix e un Postgres. Il pattern è così comune da essere quasi invisibile.

I pezzi nel dettaglio

Camminiamo attraverso le parti reali in movimento di una versione concreta di questo stack, il tipo che metteresti su domani mattina.

Il web framework

Prendi quello in cui il tuo team è più veloce. Non c’è alcuna ragione architetturale per scegliere uno specifico a questa scala. Il framework spenderà il suo tempo facendo la stessa cosa in ogni linguaggio: accetta richiesta, valida, fa query al DB, renderizza response. Qualunque framework tu scelga, impara le sue convenzioni a fondo invece di combatterle.

Due scelte opinated se non hai preferenze e vuoi qualcosa di noioso e affidabile: Django se il tuo team è in flavour Python, Rails se il tuo team è in flavour Ruby. Entrambi hanno 20 anni, entrambi sono ancora attivamente sviluppati, entrambi hanno una quantità oscena di librerie del tipo “questa cosa è già risolta” e di risposte su Stack Overflow, ed entrambi sono progettati esattamente per questa forma single-app-and-database. Phoenix (Elixir) è una scelta meno comune ma eccellente se il tuo team ha qualche esperienza con esso; scala molto più lontano di Django o Rails su una singola macchina perché la BEAM VM è genuinamente migliore con la concorrenza di CPython o MRI.

Postgres

Usa Postgres. Non agonizzarci sopra.

Postgres nel 2026 è, a essere conservativi, il database generalmente più utile mai spedito. Fa tabelle relazionali, documenti JSON (con indici!), full-text search, query geospaziali (PostGIS), time-series ragionevolmente (con TimescaleDB o partitioning), pub/sub (con LISTEN/NOTIFY), e perfino code di job (con SELECT ... FOR UPDATE SKIP LOCKED). Per la maggior parte dei workload SaaS small-to-medium, non ne uscirai mai.

Eseguilo come servizio gestito se ti puoi permettere il piccolo sovrapprezzo. Self-hostalo sulla stessa VM dell’app se non puoi. In ogni caso, abilita i backup automatici prima di avere il tuo primo utente. Faremo un’intera lezione sui backup nel modulo 9; per ora: pg_dump notturno su S3, con retention.

Connection pooling: Django e Rails lo gestiscono in-process, il che va bene finché non ti servono più di un paio di processi applicativi. Una volta che hai più processi (o, alla fine, più macchine), metti PgBouncer in modalità transaction-pooling davanti a Postgres. È il pezzo di software più noioso possibile e funziona senza lamentele da quindici anni.

La job queue

Avrai bisogno di fare lavoro in modo asincrono: spedire email, generare report, fare retry di chiamate API esterne, processare immagini caricate. L’istinto è aggiungere Redis o RabbitMQ. Resisti il più a lungo possibile, perché anche Postgres è una job queue perfettamente buona a questa scala, usando SELECT ... FOR UPDATE SKIP LOCKED per fare dequeue dei job senza contesa.

Librerie che lo rendono banale: pg-boss (Node), solid_queue (default di Rails 8, fra l’altro), dramatiq con broker Postgres, River (Go), Oban (Elixir), procrastinate (Python). Ti danno la job queue senza darti un altro pezzo di infrastruttura da operare. Quella è una vittoria significativa con un team piccolo.

Se ti serve genuinamente lavoro real-time con latenza in millisecondi (chat, presence, stato di gioco live), allora sì, Redis. Ma quello è un requisito specifico, non un default.

Il reverse proxy e il TLS

Due scelte fattibili.

Opzione A: nginx sulla VM. Termina TLS con Let’s Encrypt via certbot, fai proxy verso l’app su localhost:8000, servi i file statici direttamente da /var/www. Tre file di config, tutti da 20 righe, che copi da un tutorial la prima volta e non tocchi mai più. Gratis, veloce, va bene.

Opzione B: load balancer gestito. Cloudflare davanti gratis, o AWS ALB/Hetzner LB per pochi EUR al mese. Gestisce TLS, WAF opzionale, mitigazione DDoS opzionale. Leggermente più piacevole da operare; non devi rinnovare i certificati tu.

Vanno bene entrambe. La maggior parte dei progetti parte con l’opzione A e si sposta sulla B quando vuole una seconda VM dietro.

La pipeline di deploy

Ci sono due forme accettabili a questa scala.

Docker Compose sulla VM. Il tuo docker-compose.yml definisce l’app, Postgres, opzionalmente Redis. Fai SSH dentro, git pull, docker compose up -d --build. Fatto. I backup sono un cron job che fa pg_dumpall e pusha su S3.

systemd nudo sulla VM. La tua app gira come unit di systemd. Postgres è il servizio pacchettizzato dalla distro. I deploy sono: SSH dentro, git pull, bundle install o pip install o npm install, migrate, systemctl restart yourapp. Questo è quello che la gente faceva prima di Docker e funziona ancora. È più veloce, ha meno parti in movimento, e il modello mentale è di una unit più piccolo della versione Docker.

GitHub Actions per la CI: al push su main, esegui i test, e se è verde, fai SSH nella VM ed esegui lo script di deploy. Cinque righe di YAML. L’intero setup CI/CD è qualcosa che puoi costruire in un’ora e non toccare mai più finché non smette di adattarsi, il che per la maggior parte dei progetti è mai.

Entrambe le forme sono buone. La forma sbagliata è “stiamo per girare Kubernetes per il nostro side project da 50 MAU”, e qualcuno lo farà comunque, e passerà più tempo su YAML che su codice, e scriverà un blog post su come scala meravigliosamente, e tu non dovresti dargli retta.

Un diagramma più ricco

Quando il progetto inizia a sembrare reale (clienti paganti, il founder non è più l’unico in on-call), il quadro cresce con monitoring, backup, e una storia esplicita di deploy. Ecco che aspetto ha questa forma del passo successivo:

Diagramma da creare: un diagramma di architettura “small SaaS architecture v1”, disegnato in diagrams.net. Al centro: una VM, con tre box dentro etichettati “nginx”, “app process (gunicorn / puma / etc.)”, e “Postgres”. A sinistra: utenti finali che si connettono via HTTPS attraverso un box CDN Cloudflare. A destra: un cilindro separato “S3-compatible object storage” con frecce dalla VM che mostrano backup pg_dump notturni (etichetta: “nightly, encrypted, 30-day retention”) e file utente caricati (etichetta: “uploads”). Sotto la VM: un cluster “Prometheus + Grafana” (questi possono stare sulla stessa VM o su una piccola VM separata) con frecce verso l’alto alla VM che mostrano lo scraping delle metriche. Sotto questo: un piccolo box “Alertmanager / BetterStack” che riceve alert e li indirizza su Slack/email. In basso a destra: una pipeline di deployment mostrata come tre box da sinistra a destra: “GitHub repo” -> “GitHub Actions (run tests)” -> “SSH deploy to VM (systemd restart)”. Usa una palette colori morbida: VM in verde, storage in arancione, observability in blu, deploy in grigio. Raggruppa “production” in un box azzurro chiaro, “deployment” in un box grigio chiaro, “external services” fuori da entrambi. Non più di 12 componenti nominati in totale; il diagramma deve comunque starci in una singola schermata.

Quel diagramma rappresenta una piccola SaaS genuinamente production-grade. Un sacco di aziende nel range di ricavi tra EUR 1 e 10 M girano su qualcosa di non molto diverso. Non ha Kubernetes. Non ha un service mesh. Non ha microservizi separati. Ha le cose che gli servono davvero: TLS, backup, monitoring, alerting, e un modo per portare il codice da GitHub alla produzione in modo affidabile.

Cosa NON aggiungere ancora

Questa lista è la lezione più sottovalutata di questa lezione. Il mercato vuole venderti tutte queste cose. La maggior parte non appartiene a un sistema che non se le è ancora guadagnate.

Kubernetes. Finché non hai genuinamente bisogno di girare più servizi su più macchine con auto-scaling, Kubernetes è pagare per una capacità che non usi, e la tassa operativa è reale. Un piccolo team che gira Kubernetes spende forse un terzo del suo tempo di engineering su Kubernetes. Quello è un terzo di un team che non hai.

Microservizi. Un secondo servizio è qualcosa che separi quando un confine chiaro e duraturo è emerso nella codebase, e quando il costo della separazione (deploy separati, database separati, comunicazione asincrona, distributed tracing) è giustificato dal beneficio (scaling indipendente, confini di team, scelta del linguaggio). A tre ingegneri, quel beneficio non c’è quasi mai. Resta monolitico. L’esercizio di “spezzare in pezzi” è territorio del modulo 7 e lo faremo come si deve.

Un layer di cache separato (Redis come cache). Postgres ha un buffer pool. La maggior parte delle conversazioni “mi serve una cache” a piccola scala sono in realtà conversazioni “ho un indice mancante”. Aggiungi prima l’indice, misura, e aggiungi Redis solo se sai nominare la query specifica abbastanza calda da giustificare un layer separato.

Una queue separata (Redis, RabbitMQ, Kafka). Come sopra: Postgres-as-queue è eccellente fino a migliaia di job al minuto. Adotta una queue vera quando hai una ragione vera, non prima.

Un motore di ricerca (Elasticsearch, OpenSearch, Meilisearch). La full-text search di Postgres è genuinamente buona e gestisce forse l’80% dei casi per cui la gente raggiunge per Elastic. Parti da lì; raggiungi per il motore dedicato solo quando il relevance scoring o la search multi-tenant a scala lo costringono.

Read replica. Finché il tuo singolo primary non sta sudando, una read replica è complessità operativa per nessun beneficio. Aggiungila quando pg_stat_activity ti dice che le letture stanno schiacciando le scritture, non prima.

Un database analytics separato (Snowflake, BigQuery, ClickHouse). Finché le query analytics non interferiscono attivamente con il workload di produzione, eseguile su una read replica di Postgres. Il giorno in cui hai un warehouse da 4 TB e un data team, va bene. Quel giorno è più tardi di quanto pensi.

Service mesh, sidecar, fabric di distributed tracing. Tutti utili al loro posto. Il loro posto è lontano nel futuro per questa forma.

Il principio dietro tutte queste è lo stesso: rinvia finché il carico non lo giustifica. L’architettura è la gestione della complessità, e la complessità più economica è quella che non hai ancora aggiunto. Aggiungere un pezzo di infrastruttura è una porta a senso unico in molti team; una volta in produzione, rimuoverla è un progetto.

I team che ho ammirato di più, lavorando su sistemi con utenti reali e ricavi reali, hanno tutti girato qualcosa che assomigliava molto al diagramma di questa lezione per molto più tempo di quanto i talk delle conference suggeriscano sia possibile. Hanno scalato verticalmente, hanno tunato le query, hanno aggiunto indici, hanno tenuto la superficie operativa minuscola, e hanno speso il tempo di engineering risparmiato sul prodotto. Quella è la forma noiosa e profittevole.

Il prossimo modulo inizia il viaggio da questa architettura single-machine ai sistemi che la superano genuinamente. La lezione 7 introduce il modello C4 in profondità in modo da avere un vocabolario condiviso per disegnare questi sistemi mentre diventano più complessi. La lezione 8 è il primo passo di scaling: quando una macchina smette di bastare, cosa viene dopo, e come farlo senza perdere la semplicità che ha fatto funzionare la prima versione.

Per ora, se devi portarti via una cosa da questa lezione: la più piccola architettura che risolve il tuo problema è quasi sempre quella giusta. Puoi sempre aggiungere complessità più tardi. Quasi mai puoi toglierla.

Cerca