Search…

Trunk-based development and branching strategies

In this series (10 parts)
  1. What DevOps actually is
  2. The software delivery lifecycle
  3. Agile, Scrum, and Kanban for DevOps teams
  4. Trunk-based development and branching strategies
  5. Environments and promotion strategies
  6. Configuration management
  7. Secrets management
  8. Deployment strategies
  9. On-call culture and incident management
  10. DevOps metrics and measuring maturity

A branching strategy defines how code changes move through version control into production. The choice matters more than most teams realize. Long-lived branches create merge conflicts, delay feedback, and inflate batch sizes. Short-lived branches keep changes small, merges simple, and pipelines fast. For deeper coverage of Git branching mechanics, see branching strategies.

Feature branches

The simplest model: create a branch, do the work, open a pull request, merge to main. Most teams start here. It works well when branches live for hours or a few days.

Problems emerge when branches live longer:

  • Merge conflicts compound. The longer a branch lives, the more main diverges from it. Merging becomes increasingly painful.
  • Integration is delayed. Other developers cannot see or test your changes until you merge. Bugs that only appear when features interact stay hidden.
  • Batch size grows. A two-week branch accumulates a two-week batch of changes. Reviewing a 2000-line diff is qualitatively harder than reviewing ten 200-line diffs.

Branch lifetime is a health metric. Track it. If your average branch lives longer than two days, something in your process needs attention.

GitFlow

GitFlow uses multiple long-lived branches: main, develop, feature branches, release branches, and hotfix branches. It was designed for software with scheduled releases.

gitGraph
  commit id: "init"
  branch develop
  commit id: "dev work"
  branch feature/login
  commit id: "login UI"
  commit id: "login API"
  checkout develop
  merge feature/login id: "merge login"
  branch release/1.0
  commit id: "version bump"
  commit id: "bugfix"
  checkout main
  merge release/1.0 id: "v1.0" tag: "v1.0"
  checkout develop
  merge release/1.0 id: "back-merge"

GitFlow’s branch structure. Feature branches merge into develop, release branches stabilize a version, and main always reflects production.

When GitFlow works

  • Software shipped on physical media or through app stores with mandatory review periods
  • Products where multiple versions must be supported simultaneously
  • Teams with strict compliance requirements that mandate a separate stabilization phase

When GitFlow hurts

  • Web applications deployed continuously
  • Teams practicing CI/CD where every merge should be deployable
  • Any context where deployment frequency exceeds weekly

The develop branch in GitFlow is particularly problematic. It creates a permanent integration delay. Features merge to develop, not main. Production deploys come from main. The gap between develop and main is a gap between “integrated” and “deployed,” and that gap accumulates risk.

Trunk-based development

Trunk-based development (TBD) is the opposite philosophy: everyone commits to a single shared branch (main/trunk), either directly or through very short-lived branches that merge within hours.

gitGraph
  commit id: "init"
  branch feat-1
  commit id: "small change"
  checkout main
  merge feat-1 id: "merge feat-1"
  commit id: "direct commit"
  branch feat-2
  commit id: "another change"
  checkout main
  merge feat-2 id: "merge feat-2"
  commit id: "refactor"
  commit id: "flag off new-ui" type: HIGHLIGHT
  commit id: "direct fix"

Trunk-based development. Branches are optional and short-lived. Most work merges within hours. Feature flags control visibility of incomplete features.

How it works in practice

  1. Pull latest main.
  2. Make a small, focused change.
  3. Run tests locally.
  4. Push and open a pull request (or commit directly for small changes).
  5. Merge within hours, ideally the same day.
  6. CI/CD deploys the change to production automatically.

The discipline required: every commit to main must leave main in a deployable state. This means incomplete features cannot be committed unless they are behind a feature flag.

Why it works

  • Continuous integration becomes real. If everyone merges to main daily, integration problems surface immediately.
  • Small diffs are easy to review. Reviewers give better feedback on a 50-line change than a 500-line change.
  • Merge conflicts nearly disappear. When branches live for hours, main barely moves between branch creation and merge.
  • DORA metrics improve. The Accelerate research found that trunk-based development is a statistically significant predictor of elite delivery performance.

Release branches

Even trunk-based teams sometimes need release branches. The pattern is: cut a release branch from main when you need to stabilize for a specific release. Apply only critical fixes to the release branch. Never merge feature work into it.

The key rule: release branches are read-only except for hotfixes. New features always go to main. This prevents the release branch from becoming a parallel development track.

main:      A --- B --- C --- D --- E --- F
                  \
release/2.1:       B' --- hotfix-1 --- hotfix-2

Release branches make sense for mobile apps (where app store review creates a gap between “ready” and “released”) and for on-premise software where customers run different versions.

Feature flags as an alternative to branches

Feature flags decouple deployment from release. You deploy code to production with the new feature turned off. When the feature is ready, you toggle the flag. No branch needed. No merge needed.

This solves the core problem of long-lived branches: how do you work on a feature that takes weeks without creating a weeks-old branch? With feature flags, you merge incomplete code daily. The flag ensures users never see it until it is ready.

Feature flag lifecycle:

  1. Create flag. Define it in your flag management system.
  2. Develop behind flag. All new code paths check the flag.
  3. Test with flag on. QA and staging environments enable the flag.
  4. Progressive rollout. Enable for 1% of users, then 10%, then 50%, then 100%.
  5. Remove flag. Once the feature is fully rolled out, delete the flag and the old code path.

Step 5 is critical and often neglected. Stale feature flags accumulate as technical debt. Set an expiration date when you create the flag.

Branch lifetime as a health metric

Track the average age of open branches and pull requests. This single metric reveals a surprising amount about team health:

Average branch ageWhat it signals
Less than 1 dayTrunk-based development, continuous integration
1-3 daysHealthy feature branch workflow
1-2 weeksIntegration delays, growing review queues
More than 2 weeksBatch size problem, likely merge conflicts

If branch age is trending upward, investigate:

  • Are pull requests waiting too long for review? Add review SLAs.
  • Are work items too large? Break them into smaller increments.
  • Is the test suite too slow? Developers avoid merging because CI takes an hour.
  • Are there code ownership bottlenecks? Only one person can approve changes to certain files.

Choosing a strategy

For most web-facing teams practicing CI/CD, trunk-based development is the right default. It aligns naturally with small batch sizes, fast feedback, and continuous deployment. GitFlow adds value only when you genuinely need to maintain multiple release lines or satisfy external compliance gates that require a stabilization phase.

Start with trunk-based development. If you discover a constraint that requires more branch structure, add it deliberately. Do not start with GitFlow and try to simplify later. Removing process is harder than adding it.

What comes next

The next article covers environment strategies: how code moves from dev to staging to production, why environment parity matters, and how ephemeral environments eliminate the “works in staging” problem.

Start typing to search across all content
navigate Enter open Esc close