Animated Architecture Patterns

Trunk-Based Development

Everyone merges small changes into one main branch at least daily; feature flags hide unfinished work instead of long branches.

Also known as TBD

catalog team· 6 developers, one repository commit CI build green conflict or bug 4 weeksone release today 09:00–11:1511:15–18:0018:00 week 1week 2week 3release week 09:0010:0011:0012:0013:0014:0015:0016:0017:0018:00 develop feature/sort-options 1 feature/search-filters 3 weeks · 214 files 2 days of conflicts 2 release/2.14 3 4 no flags:unfinished work waitson its branch merge day · end of week 3 $ git merge feature/search-filters CONFLICT (content): Merge conflict in src/query.ts CONFLICT (content): Merge conflict in src/facets.ts Automatic merge failed; fix conflicts and … 2 days of fixing conflicts by hand before the merge goes in staging · release week filters break the new sorting wrong product counts integration bugs, found late fixed on release/2.14 GitFlow merges for release 2.14 1sort-options → develop 2search-filters → develop2 days 3release/2.14 → main 4release/2.14 → develop main CI passing 8 min 45 min productioncatalog service release 2.13 release 2.14 is out Mai Kai · refactor 1/3 Ben Ana Kai · refactor 2/3 Tom Lin Kai · refactor 3/3 Mai #1287 Ben ✓ sort.v2on, 7 monthspromo.bfoff, 1 year flagfilters.v2off in production deploys today: 0 deploys today: 1 deploys today: 2 deploys today: 3 deploys today: 4 deploys today: 5 deploys today: 6 deploys today: 7 deploys today: 8 deploys today: 9 mai/price-filter → main #1287 Price-range filter behind filters.v2+86 −12 open merged 09:10branchedfrom main 10:00PR opened98 lines 10:08CI passed8 min 10:40Ben approvedafter 40 min 10:50mergedbranch deleted 10:58main green8 min 11:00deployedflag off on main: merged daily, not finished if (flags.enabled("filters.v2")) return newFilters(query); return filters(query); filters.v2 is off in production, so customers still get the old filters today, catalog team 1 2 3 4 5 6 7 8 9 merges to mainfrom 6 developers each followed by a green build, 8 min branches live hours · PRs ≈ 100 lines how main stays releasable Kai's big refactor: 3 small merges (branch by abstraction) unfinished features merge dark, behind flags main goes red? fix it or revert at once no code freeze, no integration phase releases catalog: every green build of main deploys mobile app: release branches cut from main main release/5.12 fix on main first, then cherry-pick #1312: 3 days in review +640 lines and growing 1Slow or flaky CI 45 minutes and random failures:changes pile up, main stays red Aim for ~10 min, fix flaky tests 2Flags left behind sort.v2 and promo.bf are deadbranches nobody tests any more Delete each flag after release 3Reviews that take days waiting PRs grow, and peopledrift back to big branches Review within hours, or pair 4Open source, outside help contributors work in forks; amaintainer reviews every PR A different setting from one team

Step 1 · Three weeks on a branch. The catalog team used GitFlow: the new search filters lived on feature/search-filters for three weeks while feature/sort-options changed the same query code and develop moved on. Each branch built green on its own, but the merge touched 214 files and took two days of fixing conflicts, and the bugs that only appear when both features run together surfaced in staging in release week. The release branch then needed two more merges, into main and back into develop.

Step 2 · Merged the same day. Now every change starts from main and comes back the same day. Mai branches at 09:10 for the next slice of the filters, opens pull request #1287 at 10:00 with 98 changed lines, CI passes in 8 minutes, Ben approves at 10:40 and it merges at 10:50; the branch is deleted. The filters aren't finished, so the new code sits behind the filters.v2 flag, which is off in production: the green build deploys at 11:00 and customers still get the old filters (see Feature Flags).

Step 3 · One day, nine merges. By 18:00 the six developers have merged nine pull requests, each followed by a green 8-minute build on main, and the catalog service has deployed every one of them, with unfinished work behind flags. Kai's large refactor lands as three small merges using Branch by Abstraction. Teams that ship on a schedule, such as the mobile app, cut a release branch from main instead and cherry-pick fixes that were made on main first; and when a build on main turns red, the team fixes it or reverts the change at once.

Step 4 · Limits and pitfalls. Trunk-based development leans on fast, reliable CI and good automated tests: when the build takes 45 minutes or fails at random, people batch their changes and main stays red. Flags left in the code after their release (sort.v2, promo.bf) become debt, so delete them, and a pull request that waits three days for review, like #1312, pushes people back to big branches. Open-source projects with outside contributors work through forks and pull requests that maintainers review, which is a different setting from one team sharing a repository.

The problem

A branch that lives for weeks holds work nobody else has integrated. While it is open, the mainline and every other branch keep changing, so the merge at the end grows with every day it waits. Part of the damage shows up as conflicts that Git reports. The worse part merges cleanly: two changes that are each correct on their own but break each other once they run together, like Acme's new filters and the new sorting in the diagram. Nobody sees those until the combined code is tested, and on a release schedule that means a few days before the release.

Branching models built around long-lived branches add merges of their own. In GitFlow, features merge into develop, a release branch is cut from develop and then merged into main and back into develop, and hotfix branches merge into both. Vincent Driessen, who published the model in 2010, added a note in March 2020: he wrote it for software that is explicitly versioned or has several versions in use, and he steers teams doing continuous delivery towards a simpler flow such as GitHub flow. Teams that dread merge day often add code freezes and stabilization phases, which push the feedback even further out.

How it works

The best-known description is trunkbaseddevelopment.com, written by Paul Hammant with contributors. Developers work together on one branch, the trunk (main in most Git repositories), and don't create long-lived development branches; a few techniques keep the trunk working while they do. The site describes two styles:

  • Committing straight to the trunk. Each developer runs the same build that CI runs, then pushes to main. The site suggests this works for teams of up to about 15 people.
  • Short-lived feature branches. In larger teams a branch belongs to one developer (or a pair), is reviewed through a pull request and checked by CI, and is merged and deleted within a couple of days at most. Past two days, the site warns, it is turning into a long-lived branch. DORA's description is tighter: branches that typically last no more than a few hours.

Continuous integration, taken literally. Martin Fowler's Continuous Integration article (revised January 2024) asks every developer to push commits to the mainline at least once a day, every push to trigger a build, and a broken build to be fixed at once, by reverting the faulty commit if that is quicker. A build server that tests branches kept apart for weeks gives green builds without the integration. In his 2020 article on branching patterns Fowler explains that many teams use CI tools only to build feature branches, and that this dilution is part of why some people say trunk-based development instead of continuous integration. He mostly hears the two terms used as synonyms and keeps the older one.

What DORA measures. DORA lists trunk-based development as one of its capabilities and characterises it by three or fewer active branches in the repository, branches merged to trunk at least once a day, and no code freezes or integration phases. Its analysis of the 2016 and 2017 survey data found that teams following these practices reach higher software delivery and operational performance. That is a statistical association across many organisations, not a promise for any one team.

Unfinished work merges dark. A feature that takes weeks still lands in small pieces. New behaviour stays behind a feature flag that is off until it is ready, as filters.v2 is in the diagram. A large rework grows behind an interface with branch by abstraction, and database changes go in as backward-compatible steps with expand and contract. After each step main can still be released.

Releases come from the trunk. A team that practises continuous delivery releases straight from main and fixes problems forward, as the catalog service does. A team that ships on a schedule, like Acme's mobile app, cuts a release branch from main shortly before the release. A bug is reproduced and fixed on main first and the fix is then cherry-picked onto the release branch, never the other way round, and nothing is merged back. Once the release is out of use the branch is deleted, after the released commit has been tagged.

At scale. In Why Google Stores Billions of Lines of Code in a Single Repository (Communications of the ACM, July 2016), Rachel Potvin and Josh Levenberg describe trunk-based development on Google's monolithic repository. In January 2015 it held about two billion lines of code in nine million source files, shared by more than 25,000 developers who committed about 16,000 changes on a typical workday, while automated systems committed another 24,000. Nearly everyone works at head and development on branches is rare. Release branches are cut from a chosen revision and receive fixes developed on the mainline and cherry-picked, new code paths sit behind flags, and every change is reviewed before it is committed. It works because Google built tooling for it: its own repository system, Piper, plus systems for testing, code review, static analysis and large automated changes.

Putting it into practice

  1. Measure where you are. Count the active branches and their age, the merges to main per day, how long pull requests wait for review, and how many code freezes you have. These are the measures DORA suggests.
  2. Protect main and keep it green. Require a passing CI run on every pull request and run CI again on main after each merge (branch protection rules or rulesets on GitHub, protected branches on GitLab). When a build on main fails, fixing it comes first: fix it within minutes or revert the change, and nobody merges on red.
  3. Keep CI fast and reliable. Fowler repeats the Extreme Programming guideline of a ten-minute build; the catalog team's takes 8. Cache dependencies, run tests in parallel, move slow suites to later stages of the pipeline, and fix or quarantine flaky tests, because a build nobody trusts gets ignored.
  4. Make changes small. Google's engineering practices call about 100 lines a reasonable size for a change and 1,000 lines usually too large. Split work into thin vertical slices or stacked changes, and keep refactoring apart from changes in behaviour.
  5. Review within hours. Google's guide makes one business day the longest a reviewer should take to respond; the trunk-based development site aims for minutes. Pick up review requests before starting new work, and pair on hard changes: Fowler points out that pair programming is code review that never waits. Ship / Show / Ask (Rouan Wilsenach, 2021) lets some changes merge first and be reviewed afterwards, where your compliance rules allow it.
  6. Hide what isn't finished. Give each new flag an owner, a removal task and an expiry date, and delete the flag and the dead code path once the feature is fully released.
  7. Decide how you release. Release from main through a deployment pipeline, or cut short-lived release branches and cherry-pick fixes from main. Either way there is no stabilization phase on a separate branch.
  8. Add a merge queue when merges collide. At high commit rates two pull requests can pass on their own and break main together. A merge queue tests each one against the latest main plus the pull requests queued ahead of it; GitHub's merge queue and GitLab's merge trains are examples.
  9. Move off long-lived branches step by step. Stop creating new ones, finish or flag the open ones, make main the only branch that lives on, and shorten branch lifetimes from weeks to days to hours.

Where it fits

  • Continuous Delivery needs one mainline that is always releasable. Trunk-based development gets every change onto it, and the deployment pipeline takes it from there.
  • Feature Flags and Branch by Abstraction are the techniques that let unfinished and large changes merge every day; Expand and Contract does the same for database schemas. This page is about where code is integrated and how often; those pages are about how to cut the work so it can be.
  • Canary Release and Blue-Green Deployment keep the frequent deploys that follow from hurting users.
  • DORA Metrics: small, frequent merges shorten the lead time for changes and raise deployment frequency, and DORA tracks trunk-based development as a capability that drives them.
  • The Three Ways: small batches flowing through one mainline are the first way, flow, and a build after every merge is the second, fast feedback.

When to use it

  • Teams that own a service and deploy it often: fewer merges, earlier feedback, and a main that can ship at any time.
  • Any team that dreads merge day, or freezes code before a release.
  • Monorepos and shared libraries, where every long-lived branch multiplies the conflicts.

Adapt it where the setting differs:

  • Team size. A small team that pairs can commit straight to main; larger teams use short-lived branches and pull requests, as the six-person catalog team in the diagram does.
  • Regulated environments that require a second person to approve every change: mandatory pull request reviews satisfy that, as long as reviews come back within hours.
  • Legacy code with few tests or a slow build. Invest in the build and the tests first, and shorten branch lifetimes as confidence grows.
  • Installed or mobile software with several versions in use: keep a release branch per supported version and cherry-pick fixes from main.

It fits poorly where the contributors aren't one team, as in classic open-source projects: occasional contributors from outside work in forks and send pull requests that a few maintainers review. Fowler's CI article makes the same distinction. Continuous integration assumes a committed team, and feature branches with pull requests suit that other setting better.

Common pitfalls

  • Trunk-based in name only. A branch called main with pull requests that stay open for a week is still feature branching. Track branch age and review wait times, and aim for merges within a day.
  • A slow or flaky build. At 45 minutes, or with tests that fail at random, people batch their changes and stop trusting red. Keep the build near ten minutes, fix flaky tests, and run the long suites after the merge.
  • A red main left red. Everyone builds on a broken mainline. Revert within minutes, then fix the change on its branch.
  • Flags that outlive their release. Each old flag is a branch in the code that nobody tests and somebody can flip by mistake. Pete Hodgson's article on feature toggles describes teams that add a removal task with every new release flag, set expiry dates, or cap how many flags a system may have.
  • Reviews that take days. Waiting pull requests grow, and people go back to big branches to avoid the wait. Review within hours, keep changes small, and pair on the hard ones.
  • Fixing on the release branch and merging back. Fixes get lost or arrive twice. Fix on main first, then cherry-pick.
  • Freezing main before a release. A freeze stops everyone's integration at once. Cut a release branch, or release from a tag on main.
  • Copying the open-source workflow into one team. Forks and gatekeeping reviews protect maintainers from strangers' code; inside one team they only add delay. Work in one shared repository with short-lived branches.