The Branch Is Not the Feature
Most branching strategy debates are actually feature flag debates in disguise. The real question is: how do you ship code that's in progress without it breaking production? The answer is almost never βmore branchesβ β it's separating deployment from release. This article compares the main strategies and tells you which one fits which team.
- Your main branch has a 2-week-old feature branch diverging from it and someone's about to have a bad merge day
- You use GitFlow because you read about it in 2018 and never questioned it
- Every PR requires a βrebase on mainβ step that takes 30 minutes
- Your βrelease branchβ is permanently behind main but everyone's afraid to delete it
Trunk-based development with feature flags lets a 20-person team deploy 10x per day without coordination overhead.
Trunk-Based Development
Everyone commits to main(the trunk) directly, or via short-lived branches that live <2 days. Continuous integration is the literal definition: code is always integrated. You ship behind feature flags, not behind branches.
gitGraph
commit id: "fix: input validation"
branch feature/payment-v2
commit id: "feat: payment behind flag"
checkout main
merge feature/payment-v2 id: "merge (1 day old)"
commit id: "fix: typo"
commit id: "chore: deps update"
branch fix/auth-edge-case
commit id: "fix: token refresh"
checkout main
merge fix/auth-edge-case id: "merge (4 hours old)"
// Feature flag approach β ship incomplete code safely
// The flag controls visibility, not deployment
import { isFeatureEnabled } from '@/lib/flags';
export async function getCheckoutFlow(userId: string) {
const useNewPaymentFlow = await isFeatureEnabled('payment-v2', { userId });
if (useNewPaymentFlow) {
return newPaymentFlow(); // In development β only internal users see this
}
return legacyPaymentFlow(); // Everyone else
}
// Flags can be:
// - Static (env var: PAYMENT_V2_ENABLED=true)
// - User-based (LaunchDarkly, GrowthBook, Unleash)
// - Percentage rollout (5% β 25% β 100%)
// - Kill switch (disable instantly if metrics degrade)GitFlow
GitFlow uses long-lived branches: main, develop, feature/*, release/*,hotfix/*. It was designed for versioned software with scheduled releases β desktop apps, SDKs, firmware. For continuously-deployed web services, it adds coordination overhead without the benefits.
# GitFlow branch structure
main ββββββββββββββββββββββββββββββββββββββββββββββββββββββΆ
β (merge from release/1.2.0) β (hotfix)
release/1.2.0 ββββββββββββββββββββββββββ
develop ββββββββββββββββββββββββββββββββββββββββββββββββββββββΆ
β (merged features)
feature/A ββββββββ
feature/B ββββββββββββββ
# When GitFlow makes sense:
# - Versioned libraries (npm packages, SDKs)
# - Mobile apps with App Store review cycles
# - Enterprise software with quarterly releases
# - Regulated software requiring audit trails per version
# When it doesn't:
# - SaaS web applications (continuously deployed)
# - Internal tools
# - Any team deploying more than once per weekMerge Strategies
# Three merge strategies β each has different implications for git history
# 1. Merge commit β preserves full branch history
git merge feature/my-feature
# Result: creates a merge commit, full history visible
# Pro: complete context
# Con: noisy history with many small commits
# 2. Squash merge β one commit per feature
git merge --squash feature/my-feature
git commit -m "feat: add payment v2 flow (#123)"
# Result: single commit on main
# Pro: clean linear history
# Con: individual commits lost, harder to bisect
# 3. Rebase β replay commits on top of main
git rebase main feature/my-feature
git checkout main && git merge feature/my-feature
# Result: linear history, no merge commits
# Pro: clean history, each commit meaningful
# Con: rewrites SHA β problematic for shared branches
# Recommendation for trunk-based development:
# Use squash merge for feature branches (clean main history)
# Use conventional commits format for PR titles:
# feat: user authentication (#145)
# fix: token expiry edge case (#146)
# chore: upgrade to Node 20 (#147)Branch Protection Rules
# GitHub repo settings (or Terraform equivalent)
# Settings > Branches > Branch protection rules > main
branch_protection_rule:
pattern: "main"
requires:
- status_checks:
- ci/lint-typecheck
- ci/test
- ci/build
# These must be green before merge
- pull_request_reviews:
required: 1 # At least 1 approval
dismiss_stale: true # Re-approve after new pushes
- linear_history: true # Enforce squash or rebase β no merge commits
- no_force_push: true
- deletion_protected: trueHandling Hotfixes in Trunk-Based Development
# In trunk-based development, hotfixes go through the same process
# but can be fast-tracked:
# 1. Create a short-lived branch from main (which IS production)
git checkout -b fix/critical-auth-bypass main
# 2. Apply fix, test locally
git commit -m "fix: prevent token reuse after logout (#emergency)"
# 3. PR with emergency label β triggers expedited review
# CI still runs (non-negotiable β hotfixes cause incidents too)
# 4. Squash merge to main β auto-deploys to production
# No cherry-picking needed β main IS the release branch
# vs GitFlow hotfix (more complex):
# git checkout -b hotfix/1.2.1 main
# ... fix ...
# merge to main AND to develop AND to release/1.2.0
# tag 1.2.1
# (three merges for one fix)Pitfalls
Feature flags without cleanup
Every feature flag is technical debt. Flags that never get removed become permanent conditional paths that nobody fully understands. Create a ticket to remove every flag when you create it. Set a calendar reminder. Flags older than 3 months are almost certainly dead code waiting to cause a bug.
Trunk-based without CI
Trunk-based development only works if your CI pipeline is fast (<10 minutes) and reliable (<1% flake rate). Committing to trunk frequently with a slow or flaky CI means you're constantly breaking the build for everyone. Fix the pipeline before adopting TBD.
Long-lived feature branches in TBD
A βfeature branchβ that lives for 2 weeks is not trunk-based development β it's waterfall with extra steps. If a feature is too big to ship in 1-2 days, decompose it. Ship the data model, then the API, then the UI, all behind a feature flag that only enables the complete experience.

