Server

Trunk-Based Development, Practically

By Prabath Thalangama· October 24, 2026· 3 min read
#git#workflow#ci-cd

Introduction

Trunk-based development (TBD) means everyone integrates into one branch (main / trunk) frequently — at least daily — with branches living hours to a couple of days, not weeks. It's the workflow behind continuous delivery.

Why not long-lived branches

GitFlow's develop + release/* + feature/* + hotfix/* model creates:

  • Merge hell — a two-week feature branch diverges far from main; the merge is a high-risk event.
  • Delayed integration — bugs from the interaction of two features aren't found until both merge.
  • Stale branches — half-finished work rotting for months.

TBD trades this for the discipline of keeping every change small and the trunk always releasable.

How features that take weeks fit

You don't hold the code out of trunk — you hide it:

  • Feature flags — merge incomplete code behind an if flag("new-checkout") that's off in production. Turn it on when ready, for a percentage, per customer. Remove the flag once stable.
  • Branch by abstraction — introduce an interface, add the new implementation behind it, switch, delete the old — each step a small merged PR.
  • Keep migrations backward compatible (expand/contract) so a half-migrated schema still works.
if features.enabled("new-pricing", user=request.user):
    price = new_pricing_engine(cart)
else:
    price = legacy_pricing(cart)

Keeping trunk releasable

  • Required CI checks on the PR: build, unit + integration tests, lint, security scan. Red trunk blocks everyone — fix-forward or revert within minutes.
  • Branch protection: no direct pushes to main, require PR + 1 review + green checks, require branch up to date before merge (or use a merge queue).
  • Merge queue (GitHub/GitLab): serialises merges, re-runs CI on the combined result, so "green PR + green PR = red main" can't happen.
  • Small PRs. A 200-line PR gets a real review in 10 minutes; a 2,000-line one gets a rubber stamp. Small PRs are the core enabling habit.

Releasing

  • Release from trunk — tag a commit, deploy it. Every commit is a release candidate.
  • If you need a stabilisation window, cut a short-lived release/1.4 branch, cherry-pick only fixes onto it, delete it after the release. Days, not weeks.
  • Hotfix = a normal small PR to trunk, then cherry-pick to the active release branch if one exists.

Verification and troubleshooting

git log --oneline origin/main | head       # steady stream of small commits?
git branch -r --sort=-committerdate | head  # any branches older than a few days?
  • Trunk keeps breaking — PRs merging without the branch being up to date, or flaky tests being ignored. Add "require up to date" / a merge queue; quarantine flaky tests aggressively.
  • Feature flags pile up — no removal discipline. Track flags with an expiry; a stale flag is tech debt and a config-explosion risk. Review quarterly.
  • Long branches creep back — usually a too-big feature with no decomposition plan. Require a "how will this be split" note before starting.
  • Reviews are a bottleneck — PRs too big, or too few reviewers. Smaller PRs, and pair on the tricky ones instead of async review.
  • "We can't test everything on every PR, it's too slow" — parallelise, cache (see GitHub Actions cache strategies), split into a fast required set + a slower post-merge set.
  • Rollback is scary — if trunk is always releasable and deploys are small, rollback is just deploying the previous tag. If it's scary, deploys are too big.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.