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

Trunk-based development: perché la maggior parte dei team moderni è confluita qui

Branch a vita breve, feature flag, continuous integration. Il pattern adottato a scala da Google, Facebook e Microsoft, e cosa serve perché funzioni.

La lezione 49 ha introdotto le tre strategie di branching che coprono quasi tutta la pratica professionale. Gitflow con i suoi release branch formali, GitHub flow con i suoi feature branch a vita breve, e trunk-based development dove tutti committano direttamente su main e il lavoro in corso si nasconde dietro feature flag. La lezione notava che trunk-based è il pattern verso cui la maggior parte dei team moderni converge man mano che cresce, ed è il pattern che Google, Facebook e Microsoft fanno girare pubblicamente alle scale piu’ grandi. Questa lezione spiega perché.

L’argomento non è che trunk-based sia universalmente corretto. Ha dei prerequisiti. Servono i feature flag. Serve una CI solida. Richiede un cambio culturale nel modo in cui gli sviluppatori pensano al concetto di “pronto”. Il punto è che, una volta che i prerequisiti esistono, trunk-based è insolitamente ben allineato con il modo in cui il software viene costruito davvero, alla velocità reale, e le alternative iniziano a sembrare overhead piuttosto che sicurezza. Capire il pattern in profondità è utile anche se il team sceglie GitHub flow, perché ogni deriva nella direzione di GitHub flow (branch piu’ brevi, piu’ feature flag, CI piu’ veloce) è una deriva verso trunk-based, e riconoscerlo aiuta il team a fare miglioramenti incrementali senza impegnarsi in una migrazione completa.

Il pattern in un paragrafo

Ogni commit finisce su main nel giro di ore. I branch esistono ancora, ma solo per la durata della code review, e la review è su cambi piccoli (decine o poche centinaia di righe, non migliaia). I test girano a ogni push e a ogni merge. La CI è abbastanza veloce che uno sviluppatore può pushare un cambiamento e vedere il verde nel giro di pochi minuti. Il codice non ancora pronto perché gli utenti lo vedano sta comunque su main, ma è incartato in un feature flag: uno switch a runtime che controlla se quel code path si esegue. Il flag resta off finché la feature non è finita, testata e pronta per il release. Poi il flag passa a on, in produzione, spesso in modo graduale. Rilasciare diventa girare un flag, non deployare una build.

Questo è tutto il pattern. La profondità sta nei prerequisiti e nel cambio culturale.

Perché i team grandi sono confluiti qui

Tre forze spingono le organizzazioni verso trunk-based man mano che crescono.

L’evitare i conflitti domina alla scala. In un monorepo con 200 ingegneri, il costo dei merge conflict non è teorico. Branch vecchi di due settimane vanno in conflitto con migliaia di commit di cambiamenti, e la risoluzione del conflitto è pericolosa: lo sviluppatore che riscrive il merge ha contesto limitato su cosa hanno fatto le altre migliaia di commit. Branch vecchi di ore vanno appena in conflitto, perché la superficie di codice cambiato in poche ore è piccola. Un team che lavora simultaneamente su centinaia di branch, ciascuno della durata di una settimana, spenderà piu’ tempo di engineering nella risoluzione dei merge che nel lavoro vero e proprio. I branch vecchi di ore fanno sparire questo costo.

La CI scala con i commit, non con i branch. Il throughput di un sistema di CI si misura in build per unità di tempo. Un team che fa 100 commit piccoli al giorno su main sta chiedendo alla CI di girare 100 build, ciascuna su un cambio piccolo. Un team che fa 10 branch a vita lunga al giorno, ciascuno con settimane di lavoro accumulato, sta anche chiedendo alla CI di girare delle build, ma ogni build è su una superficie piu’ larga, ci mette di piu’, fallisce piu’ spesso e produce meno segnale azionabile quando fallisce. Trunk-based development produce naturalmente il pattern di carico in cui la CI riesce meglio: tanti cambi piccoli, ciascuno trattabile individualmente.

Il continuous deployment diventa possibile. Quando ogni commit su main è pronto per l’integration, la pipeline di deploy può essere innescata a ogni merge senza intervento umano. Alcune organizzazioni lo fanno letteralmente; altre raggruppano i deploy a cadenza oraria o giornaliera ma trattano comunque ogni commit su main come deploy-eligible. La scelta tra batch e continuous deploy diventa operativa piuttosto che architetturale, perché l’architettura (ogni commit è rilasciabile) supporta entrambe le opzioni.

La combinazione è ciò che fa funzionare il pattern alla scala. I conflitti spariscono perché i branch sono brevi. La CI gestisce bene i cambi piccoli, quindi il feedback è veloce. Il continuous deployment diventa routine perché ogni commit è pronto. Il volano si rinforza da solo: feedback piu’ veloce incoraggia cambi piu’ piccoli, cambi piu’ piccoli riducono ulteriormente i conflitti, meno conflitti incoraggiano gli sviluppatori a committare ancora piu’ spesso.

I prerequisiti

Il pattern funziona solo se ci sono quattro pezzi al loro posto. Saltane anche uno e trunk-based diventa doloroso o pericoloso.

Feature flag su scala. Un feature flag è uno switch a runtime che controlla se il codice gira. L’implementazione piu’ semplice è un valore di configurazione letto a request time: if config.features.new_checkout: handle_with_new_logic() else: handle_with_old_logic(). Le implementazioni production-grade sono piu’ ricche: targeting per utente, rollout percentuali, kill switch, abilitazioni schedulate, audit log di chi ha girato cosa e quando. Le offerte commerciali includono LaunchDarkly, Unleash, ConfigCat, GrowthBook e Statsig. Lo sforzo di standardizzazione è OpenFeature (https://openfeature.dev/, consultato 2026-05-01), un progetto CNCF che fornisce un SDK vendor-neutral cosi’ i team possono cambiare provider di flag senza riscrivere il codice applicativo. Molti team costruiscono un sistema fatto in casa, specialmente nelle organizzazioni piu’ grandi dove i requisiti sono specifici.

La capability che conta è dinamica. Il codice mergiato su main viene shippato, ma il flag resta off finché il team non è pronto. Il flag può essere acceso prima per una piccola percentuale di utenti, monitorato, espanso, e fatto rollback istantaneamente se qualcosa va male. È questo il meccanismo che rende sicuro il “ogni commit fa deploy”, perché il deploy non è il release: il flip del flag è il release.

CI solida. Ogni push su un branch e ogni merge su main fa girare la suite di test. L’asticella è alta: la pipeline di CI deve essere abbastanza veloce che gli sviluppatori aspettino minuti, non ore, per il feedback, e abbastanza affidabile che una build rossa significhi un fallimento vero piuttosto che un flake. Un team che lascia che le build rosse si trascinino su main ha perso la proprietà che rende sicuro trunk-based, perché all’improvviso nessuno sa quali commit hanno rotto le cose e la linea di integration smette di essere una fondamenta stabile.

Le pratiche meccaniche che supportano tutto questo. CI pre-merge obbligatoria: nessun commit atterra su main senza una build verde sul cambio proposto. Le branch protection rule in GitHub o GitLab impongono questo senza affidarsi alla disciplina degli sviluppatori. Parallelismo dei test in modo che la suite finisca in minuti anche mentre cresce. Eliminazione aggressiva dei flake, perché i test flaky addestrano gli sviluppatori a ignorare i fallimenti di CI, e questo distrugge la linea di integration.

Code review su cambi piccoli. Le pull request hanno ore di vita, non settimane. Il diff è abbastanza piccolo che un reviewer possa leggerlo in quindici minuti. L’autore non ci ha investito due settimane di lavoro, quindi il feedback è economico da applicare. Questa è una proprietà tanto culturale quanto tecnica: il team deve interiorizzare che PR piccole e frequenti sono meglio di PR grandi e infrequenti, e il tooling deve supportarlo. GitHub, GitLab, Gerrit e Phabricator hanno tutti idiomi per PR concatenate o stacked che permettono a uno sviluppatore di spezzare un cambio logicamente grande in una sequenza di pezzi piccoli e revisionabili.

Infrastruttura di test che cattura i problemi al PR time. La suite di test non è solo unit test. Include integration test contro ambienti effimeri, contract test tra servizi, suite di regressione per modalità di fallimento note, e (per il lavoro sui dati) il tipo di test pipeline-level che la lezione 51 copre. L’investimento è significativo. Il payoff è che i problemi affiorano sulla PR piuttosto che su main, il che significa che costano qualche minuto da fixare invece di un incidente in produzione da cui riprendersi.

Il cambio culturale

I prerequisiti tecnici sono la parte piu’ facile. La parte piu’ difficile è il cambio culturale nel modo in cui gli sviluppatori pensano al concetto di “pronto”.

In gitflow o in un GitHub flow con branch lunghi, il modello mentale dello sviluppatore è “mergerò questo quando è finito”. Il branch è uno spazio di lavoro privato; il merge è il momento in cui il lavoro diventa pubblico. Il branch può restare privato per tutto il tempo che il lavoro richiede. Gli errori sono recuperabili sul branch, perché niente è ancora stato integrato.

In trunk-based, il modello mentale dello sviluppatore è “atterrerò questo al buio e lo finirò su main”. Il lavoro diventa pubblico al primo commit. Il primo commit è piccolo (una funzione stub, un default del flag a off, una tabella su cui ancora niente scrive) e va su main. I commit successivi mettono carne al fuoco sull’implementazione. Il flag resta off per tutto il tempo, quindi gli utenti non vedono cambiamenti di comportamento, ma il codice è su main, integrandosi con tutto il resto, ottenendo il beneficio di CI e review a ogni incremento.

Lo spostamento è scomodo per gli sviluppatori abituati al modello piu’ vecchio. Saltano fuori tre obiezioni specifiche.

“E se il mio codice non è pronto per la produzione?” Trunk-based non richiede che il codice sia pronto per la produzione. Richiede che il codice sia sicuro da deployare. Una funzione che esiste, è testata ed è irraggiungibile (perché niente la chiama, o perché il flag è off) è sicura da deployare. La readiness verso l’utente è disaccoppiata dal deploy.

“E se il mio codice è a metà?” Una funzione a metà mergia comunque se ha un test e un flag. Il test verifica che la funzione a metà si comporti nel modo in cui attualmente sostiene di comportarsi. Il flag garantisce che non venga usata. La PR successiva aggiunge piu’ funzionalità, piu’ test, e il flag resta off finché la cosa intera non funziona. Ogni PR è piccola, mergiabile individualmente, e sicura individualmente.

“E i grossi cambiamenti architetturali?” Il pattern è la strategia parallel-implementation. La nuova architettura viene costruita accanto a quella vecchia. Entrambe vanno in produzione. Il flag controlla quale gira per quali utenti. La migrazione è un flip graduale del flag dal vecchio al nuovo, con l’opzione di tornare indietro se qualcosa va male. Il vecchio codice viene cancellato solo quando la migrazione è completa e stabile. Questo è piu’ lavoro di una riscrittura one-shot, ed è drammaticamente piu’ sicuro, ed è il pattern dominante per le migrazioni nelle grandi organizzazioni di engineering proprio perché le proprietà di sicurezza contano a quella scala.

flowchart TB
    subgraph build ["Build phase: small commits to main"]
        direction LR
        c1["init"] --> c2["flag<br/>stub off"] --> c3["small<br/>fix"] --> c4["step 1"] --> c5["step 2"] --> c6["step 3"] --> c7["tests"]
    end

    subgraph rollout ["Rollout phase: flip the flag"]
        direction LR
        c8["flag on<br/>5%"] --> c9["flag on<br/>50%"] --> c10["flag on<br/>100%"] --> c11["remove<br/>flag"]
    end

    build --> rollout
    rollout --> next["next feature, same loop"]

    classDef commit fill:#0d9488,stroke:#0d9488,color:#ffffff
    classDef flag fill:#fff5e6,stroke:#c89200,color:#5a3e00
    classDef boundary fill:transparent,stroke:#0d9488,stroke-dasharray: 5 5
    class c1,c2,c3,c4,c5,c6,c7,next commit
    class c8,c9,c10,c11 flag
    class build,rollout boundary

Diagramma da creare: un gitGraph rifinito di trunk-based con tanti piccoli commit continui su main. Ogni commit è piccolo. La sequenza “flag on” mostra il rollout graduale: 5%, 50%, 100%, rimuovi flag. Il punto visivo è che non ci sono branch nel diagramma, il lavoro è una sequenza di piccoli commit, e il release è una serie di flip del flag piuttosto che un deploy.

I case study

La credibilità del pattern viene in parte dai case study pubblici. Il monorepo di Google con migliaia di ingegneri che committano su un singolo repository, descritto nel paper ACM del 2016 di Rachel Potvin e Josh Levenberg “Why Google Stores Billions of Lines of Code in a Single Repository”, è l’esempio canonico: trunk-based development alla scala piu’ grande mai documentata pubblicamente. Il monorepo di Facebook è simile per forma ed è stato descritto sul Facebook engineering blog nel periodo dal 2014 al 2018. Il passaggio di Microsoft della codebase di Azure DevOps a trunk-based è stato documentato in dettaglio in una serie di post del 2018 sul blog di engineering dell’azienda.

Esempi di scala intermedia includono la pipeline di deploy di Etsy (resa famosa attraverso il blog di engineering di Etsy e i talk di John Allspaw e Ian Malpass tra il 2010 e il 2015) e le pratiche pubblicamente documentate di Spotify, Booking.com e molti altri. Il pattern non è esotico; è la baseline operativa nella maggior parte delle organizzazioni di engineering con piu’ di un paio di centinaia di ingegneri, e in molte piu’ piccole pure.

Il sito trunk-based (https://trunkbaseddevelopment.com, consultato 2026-05-01) mantiene una lista piu’ lunga di case study e un corpus sostanzioso di guida pratica. Il libro “Accelerate” di Forsgren, Humble e Kim (IT Revolution, 2018) si appoggia alla ricerca State of DevOps e identifica trunk-based development come una delle pratiche statisticamente associate alle organizzazioni di engineering ad alte prestazioni. La correlazione non prova la causalità, ma l’evidenza è abbastanza coerente che i difensori del pattern lo trattano come acquisito.

Quando trunk-based non è la scelta giusta

Il pattern non è universale. Tre popolazioni sono servite meglio da qualcos’altro.

Team piccoli senza infrastruttura di feature flag. Un team di quattro persone che lavora su un tool interno non ha bisogno dell’overhead di feature flag, ambienti effimeri e implementazioni parallele. La complessità di trunk-based supera il suo beneficio a questa scala. GitHub flow con branch della durata di un giorno porta il team quasi dove sarebbe arrivato comunque, con molto meno investimento in infrastruttura.

Prodotti a release versionati. App mobile, librerie, software on-prem e firmware embedded hanno tutti un processo di release in cui “quello che abbiamo shippato” è significativamente diverso da “quello su cui stiamo lavorando”. Il modello di branching di gitflow cattura quella differenza esplicitamente. Trunk-based si può far funzionare per questi prodotti (specialmente con i feature flag), ma il fit naturale è gitflow, e il team dovrebbe resistere alla voglia di usare il pattern piu’ alla moda solo per il gusto di essere alla moda.

Ambienti regolatori in cui ogni release è esplicito. Dispositivi medici, sistemi core bancari, software aerospaziale e altri settori con approvazione regolatoria formale per ogni release non possono deployare a ogni commit. Il regolatore vuole sapere cosa c’è in questa release, firmato da chi, validato contro quale suite di test. Trunk-based development è incompatibile con quel requisito, perché la cadenza è sbagliata: non c’è “questa release”, c’è solo “quello che è su main adesso”. Il pattern che si adatta qui è piu’ vicino a gitflow con release branch formali, sign-off formali e una pipeline di deploy che include la review regolatoria come gate.

L’angolazione data-pipeline

Le data pipeline possono usare trunk-based development se l’infrastruttura di integration test (lezione 51) è abbastanza buona e i job sono abbastanza idempotenti (lezione 38) da reggere il re-running su dati cattivi.

Il principio è lo stesso del codice di servizio: il cambio viene shippato su main, gira in produzione dietro un flag, ed è validato contro dati reali prima di essere promosso. Per le pipeline batch questo tipicamente significa scrivere la nuova trasformazione accanto a quella vecchia, in una tabella di output separata, e confrontare i due output prima di fare il cutover. Per le pipeline streaming significa far girare sia il vecchio sia il nuovo processore contro lo stesso input stream e confrontare i loro output. In entrambi i casi, il ruolo del flag è lo stesso che in un servizio: disaccoppia il deploy dal release, cosi’ il team può deployare frequentemente senza impegnarsi a un cambio di comportamento ogni volta.

La prossima lezione va a fondo sull’infrastruttura di testing che rende sicuro tutto questo per il lavoro sui dati, perché trunk-based development senza fiducia nei test è solo caos con un nome piu’ elegante.

Citazioni e letture di approfondimento

  • Paul Hammant e collaboratori, “Trunk-Based Development”, https://trunkbaseddevelopment.com (consultato 2026-05-01). Il sito di riferimento, con guida pratica dettagliata e case study.
  • Rachel Potvin e Josh Levenberg, “Why Google Stores Billions of Lines of Code in a Single Repository”, Communications of the ACM, luglio 2016. La descrizione canonica di monorepo-piu’-trunk-based alla scala di Google.
  • Nicole Forsgren, Jez Humble e Gene Kim, “Accelerate: The Science of Lean Software and DevOps” (IT Revolution, 2018). Il caso empirico per trunk-based development come pratica ad alte prestazioni.
  • Progetto OpenFeature, https://openfeature.dev/ (consultato 2026-05-01). L’SDK e la specifica vendor-neutral per i feature flag.
  • Blog di engineering di LaunchDarkly e documentazione di Unleash, entrambi linkati dal sito di OpenFeature, per esempi production-grade di sistemi di feature flag e i pattern costruiti intorno a essi.
Cerca