GitGENERALSCALE-SPECIFICORG-SPECIFIC

Trunk-Based Development

Small changes, integrated into one mainline at least daily, with main kept releasable at all times — which is a set of demanding requirements, not a branch naming convention.

The question, the obvious approach, and why it breaks

Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.

The production question

What does it actually take to keep one mainline releasable while everybody integrates into it every day?

The problem

Integration cost grows with divergence, and divergence grows with time. Working on a shared mainline removes that cost — but only if unfinished work can reach main without reaching users, and only if you find out within minutes when main breaks.

What teams do first

Everyone works off main with short branches and merges quickly. That is the whole practice: shorter branches, less merge pain.

How it breaks

Without a way to hide unfinished behaviour at runtime, integrating early means shipping half a feature. Teams discover this, retreat to long branches, and conclude the practice does not work (Feature Flags: Deploy Is Not Release).

How it breaks in production
  • Without a way to hide unfinished behaviour at runtime, integrating early means shipping half a feature. Teams discover this, retreat to long branches, and conclude the practice does not work (Feature Flags: Deploy Is Not Release).
  • Without fast CI, a fifteen-minute change waits an hour for feedback, so people batch work to amortise the wait — and the batches are long-lived branches with a different name (CI Is a Feedback System).
  • With flaky tests, red on main becomes ambiguous, and an ambiguous signal gets ignored. Once red is normal, main is not releasable and nobody notices the day it genuinely breaks (Flaky Tests).
  • Without review turnaround measured in hours, the branch lifetime is set by the reviewer's calendar regardless of intent.
  • Without discipline about main being releasable, "we will fix it forward" accumulates into a mainline that needs a stabilisation pass — which is the thing release branches were invented for.
CodeBuildTestArtifactReleaseDeployRunObserveOperateIncidentRecoverLearnImprove

What is actually happening

Underneath the tooling, which is the part that survives a change of tool.

  • One mainline is the integration point. Every change is merged into it frequently — the usual guidance is at least daily, and the real target is short enough that no two people are working from meaningfully different states.
  • Two kinds of conflict matter. Textual conflicts the tool detects and a human resolves. Semantic conflicts — both sides merge cleanly and the result is wrong, because one side changed an assumption the other side relied on — are undetectable by merge and grow with branch age.
  • Integrating early converts semantic conflicts into test failures on main, where they are cheap. Integrating late converts them into production defects, where they are not.
  • Feature flags decouple "code is in main" from "behaviour is live". That decoupling is what makes daily integration compatible with multi-week features (Feature Flags: Deploy Is Not Release).
  • Branch by abstraction is the structural alternative for changes too invasive to flag: introduce an abstraction over the old implementation, add the new implementation behind it, switch, remove. Every step is mergeable and every step keeps main working.
  • The mainline's releasability is maintained by the checks that run before merge. Which is why "required checks" is not bureaucracy here — it is the mechanism the whole practice rests on (Required Checks).

The preconditions, in the order they have to exist

Adopting these out of order is the most common way the practice fails. Shortening branches before fast CI and flags exist produces a broken main and a team that correctly concludes something is wrong.

What has to be true before short-lived branches help
  1. 1
    Fast CI

    Returns a verdict in minutes, so integrating is cheaper than batching.

    fails by A slow pipeline makes batching rational, and batching recreates long-lived branches (Designing the Pipeline).

    evidence Median time from push to verdict, measured; short enough that nobody context-switches away.

  2. 2
    Trustworthy tests

    Red means broken, so a red main is an interrupt rather than an opinion.

    fails by Known flakes teach the team to re-run, and the signal stops being read (Flaky Tests).

    evidence Re-run rate on failures is low; flaky tests are tracked and fixed, not muted.

  3. 3
    Runtime hiding

    Unfinished behaviour can be in main without being live.

    fails by No flags, so integrating means exposing (Feature Flags: Deploy Is Not Release).

    evidence A flag exists, has an owner, and has been flipped in production.

  4. 4
    Slicing skill

    Work is decomposed into independently-correct increments.

    fails by Increments that are only correct together, so main is broken between them.

    evidence Each merged commit could ship on its own (Change Size: Why Small Changes Are Safer, and When They Are Not).

  5. 5
    Fast review

    Review completes in hours, so it does not set branch lifetime.

    fails by Reviewer availability becomes the real branching policy (Review as a Gate).

    evidence Time to first review comment, measured.

  6. 6
    Checks on the merge result

    What gets tested is what will be on main after merging.

    fails by Two individually-green changes break in combination (Required Checks).

    evidence The pipeline reports the merge commit it evaluated, not just the branch head.

  7. 7
    Shared discipline

    A broken main is fixed immediately by whoever broke it.

    fails by Main red for hours because it is nobody's current priority.

    evidence Time-to-green on main after a break, tracked.

Only the last step is cultural. The other six are engineering investments, which is why "adopt trunk-based development" is usually a CI and testing project wearing a branching label.

A multi-week feature, integrated daily

SIMULATEDAn illustrative reconstruction, not a measurement — the day numbers and the conflict count are made up to show shape. What transfers is the structure: divergence paid once and late versus paid continuously and cheaply, and risk concentrated in one deploy versus spread across reversible config changes.

The concrete objection is always the same: a feature takes three weeks, so how can it be merged daily? The answer is that the feature is not merged daily — increments of it are, each one correct on its own, with the behaviour hidden until the last step.

Same feature, two integration strategies
Feature branch, merged at the end
day  1  branch created
day  1-18 47 commits, no integration
day 19  rebase onto main: 14 conflicts
day 19  suite fails for reasons unrelated to the feature
day 20  merged; a semantic conflict with a refactor from day 6
        survives the merge and ships
review: 4,100 line diff, approved in 20 minutes
Sliced, flagged, integrated daily
day  1  new module added, unreferenced        -> merged
day  2  flag added, default off               -> merged
day  4  read path behind the flag             -> merged
day  7  write path behind the flag            -> merged
day 11  backfill job, off by default          -> merged
day 14  flag on for internal users            -> config change
day 17  flag to 5%, compared against baseline -> config change
day 19  flag to 100%
day 26  flag and old path removed             -> merged

The first version accumulates eighteen days of divergence and pays for all of it in one merge, one review and one deploy — and the semantic conflict with the day-6 refactor is invisible to both the merge tool and the reviewer. The second never diverges by more than a day, and the risky moments are flag flips, which are reversible in seconds. The total work is similar; the distribution of risk is not.

Where it stops being the right answer

Being specific about the boundaries is what separates a practice from a doctrine. These are the cases where a longer-lived branch is the correct engineering decision rather than a failure of discipline.

  • Supported release lines. Customers on old versions need fixes; that requires a branch per line and a backport process (Git Workflows).
  • Regulated release trains. Where release content is frozen and validated before a scheduled date, a stabilisation branch is the mechanism that freeze is implemented with.
  • Sweeping mechanical changes. A framework or language upgrade touching every file cannot be usefully sliced; a short, coordinated branch with a merge window is honest, and branch by abstraction is the alternative where the change is behavioural rather than mechanical.
  • Untrusted contributors. Fork-based contribution is a trust boundary, not an integration strategy, and it is compatible with trunk-based development on the maintainer side.
  • Preconditions genuinely absent. With an hour-long flaky suite and no flags, shortening branches makes things worse. Fix the preconditions first; the branching change is the last step.

How to do it properly

Most important first.

  • Get CI fast first. Everything else in this practice is downstream of feedback latency; if CI is slow, fix that before changing branching (Designing the Pipeline).
  • Fix flaky tests as production defects in your delivery system, not as an annoyance to re-run. A suite with known flakes cannot keep main releasable (Flaky Tests).
  • Introduce feature flags before shortening branches, so there is somewhere for unfinished work to hide (Feature Flags: Deploy Is Not Release).
  • Slice work so each merge is independently correct — additive first, switch later, remove last (Plan and Code).
  • Treat a broken main as the highest-priority interrupt for whoever broke it. The releasability of main is a shared asset and it degrades quickly once it is optional.
  • Keep review turnaround in hours. If review is the bottleneck, that is the thing to fix, not the branching model (Review as a Gate).
  • Retire flags on a schedule. A flag with no expiry becomes a permanent untested branch in the code (Feature Flags: Deploy Is Not Release).

How much can this affect

Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.

Blast radius if this is wrongEveryone
One testEveryone
What contains it

Feature flags and progressive rollout. Without flags, everything merged into a continuously-deployed main reaches everyone, which is precisely why flags are a precondition rather than an accessory.

What can go wrong

Failure modes, including of the mitigation
  • Flag debt: dozens of flags, unclear ownership, combinations nobody tests, and a flag evaluation path that is itself a source of incidents.
  • Main broken for hours because the break was detected after merge and nobody owned fixing it.
  • Merge races: two changes each green individually, broken in combination, because checks never ran on the merge result (Required Checks).
  • Adopting the branching part without the testing part, producing faster integration of less-verified changes — genuinely worse than what it replaced.
  • Very large changes that cannot be sliced — a framework upgrade touching everything — forced through daily integration, where branch by abstraction or a short-lived coordinated branch is the honest answer.
Misreads this invites
  • "Trunk-based means no branches." It means short-lived ones. The variant with no branches at all is committing directly to main, which is viable for small co-located teams with strong pairing and unusual elsewhere.
  • "It means no code review." Review happens; it happens on small changes quickly, or through pairing.
  • "Shipping to main means shipping to users." Only if deploy and release are the same event, which is exactly what flags separate (Deployment Is Not Release).
  • "It works because branches are bad." It works because integration cost is superlinear in divergence. The branch is not the problem; the delay is.
  • "Adopt it and delivery improves." Adopt the preconditions and delivery improves. The branching change is the last step, not the first.

Operating it

Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.

How you know it worked
  • Median branch lifetime is under a day, measured rather than asserted.
  • Main has been green for the overwhelming majority of the last month, and the exceptions have known durations.
  • You could cut a release from main right now without a stabilisation pass.
  • Flag count is stable or falling — flags are being removed at roughly the rate they are added.
  • Time from merge to a deployable artifact is short enough that nobody batches to avoid it.
How you get back
  • A bad change on main is reverted as a new commit, which is fast and reviewable, and is the normal recovery.
  • Behaviour behind a flag is withdrawn by flipping the flag — seconds, and no deployment (Feature Flags: Deploy Is Not Release).
  • Production rollback is redeploying the previous artifact digest, independent of what main looks like (Rollback: Only Useful If It Is Actually Safe).
  • Retreating from the practice itself is possible and should be honest about why: usually CI speed or test reliability, both of which are fixable and neither of which is a branching problem.
What to automate, and what stays human
  • Automate: pre-merge checks on the merge result, merge queues where merge order matters, flag inventory reporting, and stale-flag alerts.
  • Automate the fast feedback — parallelisation and caching in CI are what make this practice affordable (Parallelising CI).
  • Keep human: deciding when a flag is safe to remove, and deciding whether an invasive change should be sliced or done behind an abstraction (The Automation Trap).
What this costs
  • Requires investment in testing and CI before it returns anything. Adopted without that investment it is a downgrade.
  • Feature flags add runtime complexity, a configuration surface with its own blast radius, and code paths that are not exercised while off (A Config Change Is a Production Change).
  • Slicing invasive work into independently-correct increments is genuinely harder engineering than doing it all at once on a branch.
  • Not a fit where release lines must be supported in parallel — in that case you need both this and release branches (Git Workflows).

Where this applies

This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.

  • GENERALThe relationship between divergence time and integration cost is a property of concurrent editing, not of any tool. Every version control system has it; Git makes textual merges cheap and does nothing about semantic ones.
  • SCALE-SPECIFICOn a repository with a handful of contributors, merge order rarely matters and required checks on the branch are enough. Above roughly fifty active contributors, near-simultaneous merges make branch-only checks unreliable and a merge queue becomes necessary, at the cost of merge latency (Required Checks).
  • ORG-SPECIFICIt flips where release lines must be supported in parallel — regulated release trains, on-premise software, published SDKs. There, trunk-based describes how work reaches main, and release branches still carry what customers run (Git Workflows).

Where the depth lives

This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.

Observability & Performanceperformance-regression-detection
Domains that do not exist yet
  • Testing & Reliability Engineering — the suite's speed and trustworthiness are the two variables that decide whether this practice is available at all.