Trunk-Based Development #
Trunk-Based Development (TBD) is the philosophical opposite of Gitflow — instead of separating work into many branches living for days or weeks, all developers commit directly to one main branch (the trunk) or to short-lived branches no older than one day. This sounds dangerous to anyone used to Gitflow. How can everyone commit to one place without breaking each other? The answer lies in a different discipline: small and frequent commits, very strict and fast CI, feature flags to hide unfinished features, and an engineering culture prioritizing continuous integration over long isolation. This article covers TBD from its core principles, how it works in practice, feature flags as its main pillar, and when TBD is the right choice versus Gitflow.
The Problems TBD Solves #
To understand why TBD exists, you need to understand the problems that often arise with long-lived branches like those in Gitflow:
Problems with long-lived branches:
1. Merge Hell
A feature branch lives 2 weeks → develop has changed drastically
→ Merge conflicts eating hours or days
→ Slow integration testing
→ Developers fear merging → branches grow longer → problems grow bigger
2. Delayed Integration
Features are worked on separately → integration bugs only surface
at the sprint-end merge
→ Late discovery of integration issues
→ Hard to find root causes when many changes land at once
3. Ever-Growing Divergence
Every day a branch isn't merged = one more day of divergence
→ The longer it goes, the harder to merge
→ Teams sometimes avoid merging "later" → it piles up
4. Expensive Context Switching
Reviewing large PRs (500+ lines of changes) = high cognitive load
→ Reviewers often don't review thoroughly
→ Bugs that should have been caught in review slip into production
TBD addresses these with the opposite philosophy: integrate more often, not less often. An integration problem detected at the last minute of a sprint is far more expensive than the same problem detected 5 minutes after being committed.
Core Trunk-Based Development Principles #
flowchart TD
subgraph TBD["Trunk-Based Development"]
Dev1["Developer A"]
Dev2["Developer B"]
Dev3["Developer C"]
Trunk["main / trunk\n(always deployable)"]
CI["CI Pipeline\n(runs per commit)"]
Prod["Production"]
Dev1 -->|"small commits every\nfew hours"| Trunk
Dev2 -->|"small commits every\nfew hours"| Trunk
Dev3 -->|"small commits every\nfew hours"| Trunk
Trunk --> CI
CI -->|"green → deploy\n(can be multiple times a day)"| Prod
end
style Trunk fill:#27AE60,color:#fff
style CI fill:#E67E22,color:#fffPrinciple 1: One Always-Deployable Branch #
The trunk (usually main) is always in a state deployable to production at any time. This isn’t an aspiration — it’s a constraint forcing the team to maintain quality in every commit.
What "always deployable" means:
→ All tests must pass on every commit
→ The build must succeed on every commit
→ No broken dependencies
→ No half-finished features active (use feature flags)
How to maintain it:
→ Fast CI (ideally < 10 minutes for initial feedback)
→ Meaningful test coverage
→ Developers must not push when CI is red
→ Feature flags for code not ready to publish to users
Principle 2: Small, Frequent Commits #
Ideal commit size in TBD:
→ Every commit changes at most 1 logical thing
→ Every commit leaves the codebase in a better (or equal) state
→ Developers commit at least 1-2 times a day, ideally more often
→ Easier code review: small changes = easier to understand
Comparison with Gitflow:
Gitflow: PRs contain 200-500 lines of changes after days of work
TBD: Commits contain 20-50 lines of changes after hours of work
Reviewers understand 50 lines more easily → more thorough reviews
Bugs are easier to trace → every commit is small and meaningful
Rollbacks are easier → every commit can be reverted without big impact
Principle 3: Continuous Integration #
"Continuous" in Continuous Integration really means continuous,
not just "once a day" or "when creating a PR":
CI targets in TBD:
→ Every push to the trunk triggers the CI pipeline
→ The CI pipeline must finish in < 10 minutes (ideally < 5 minutes)
→ Developers get feedback before moving to other work
→ If CI is red: this is a BLOCKING priority — fix it now
Why CI must be fast:
Developer commits → waits 30 minutes → CI red → already forgot the context
→ Slow CI = developers start bypassing CI = CI loses its value
Developer commits → waits 5 minutes → CI red → still remembers what changed
→ Fast CI = developers always in a tight loop with code quality
Short-Lived Feature Branches — The Pragmatic Compromise #
Pure TBD means all commits go straight to the trunk. But there’s a more pragmatic variation widely used — short-lived feature branches that never live longer than one working day.
flowchart LR
subgraph Gitflow["Gitflow — Long-Lived"]
GF["feature/checkout\n(lives 2 weeks)"]
GD["develop"]
GF -->|"merge after\n2 weeks"| GD
end
subgraph TBDPure["Pure TBD"]
TC1["commit A (Dev A)"]
TC2["commit B (Dev B)"]
TC3["commit C (Dev A)"]
TT["trunk/main"]
TC1 --> TT
TC2 --> TT
TC3 --> TT
end
subgraph TBDShort["TBD with Short-Lived Branches"]
SF["feature/add-button\n(lives < 1 day)"]
ST["trunk/main"]
SF -->|"merge within\nhours"| ST
end
style GF fill:#E74C3C,color:#fff
style TT fill:#27AE60,color:#fff
style ST fill:#27AE60,color:#fffShort-lived branch rules in TBD:
Time limits:
→ Branches must not live longer than 1 working day (ideally < 4 hours)
→ If longer: break the work down into smaller pieces
→ No "but the feature isn't finished" — use feature flags
Size limits:
→ A branch contains at most 1 small logical unit of change
→ No "implement everything in one go"
Merging to the trunk:
→ No long reviews — reviewers must be able to review in < 30 minutes
→ If a review takes > 30 minutes: the PR is too big, break it down
One hallmark of good TBD:
The trunk has dozens of commits per day from many developers
(not 1-2 big commits per week per developer)
Feature Flags — TBD’s Main Pillar #
This is the mechanism making TBD possible for features that can’t be finished in one day. Feature flags let code already merged into the trunk stay hidden from users until ready to publish.
// The simplest feature flag — an environment variable
func RenderCheckout(cart Cart) Component {
showNewCheckout := os.Getenv("FEATURE_NEW_CHECKOUT") == "true"
if showNewCheckout {
return NewCheckoutFlow(cart) // not active in production yet
}
return OldCheckoutFlow(cart) // default for all users
}
// A more advanced feature flag — gradual rollout
func RenderCheckoutWithRollout(cart Cart, userID string) Component {
isInExperiment := featureFlags.IsEnabled("new_checkout", featureFlags.Options{
UserID: userID,
RolloutPercentage: 10, // active for only 10% of users
})
if isInExperiment {
return NewCheckoutFlow(cart)
}
return OldCheckoutFlow(cart)
}
Feature flag types by purpose:
1. Release Flags (most common in TBD)
Hide unfinished or not-yet-ready features from all users
Example: a new feature still in development
Lifecycle: created → enabled when ready → REMOVED from code
2. Experiment Flags (A/B Testing)
Enable different variations for user subsets
Example: a new layout for 50% of users, the old layout for the other 50%
Lifecycle: created → experimented → winner chosen → flag removed
3. Ops Flags (Operational/Kill Switches)
Turn off a specific feature when there's a production problem
Example: disable the AI recommendation feature if its service is down
Lifecycle: permanent (but rarely toggled)
4. Permission Flags
Enable features only for specific user segments
Example: premium features only for paying subscribers
Lifecycle: semi-permanent
Feature flags are technical debt that must be paid. Every flag not removed after a feature launches is dead code making the codebase harder to understand. Set a team policy: every release flag must be removed within 1-2 sprints after the feature reaches 100% rollout. Use tools to track inactive flags.
The Right CI Pipeline for TBD #
CI is TBD’s main gatekeeper. It must be fast enough to give meaningful feedback before developers move on to something else.
# GitHub Actions — TBD CI optimized for speed
name: CI
on:
push:
branches: [main] # every push to the trunk
pull_request:
branches: [main] # every PR to the trunk
jobs:
# Stage 1: Fast feedback (< 2 minutes)
quick-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint
run: make lint # ~30 seconds
- name: Type check
run: make typecheck # ~30 seconds
- name: Unit tests
run: make test-unit # ~60 seconds
# Stage 2: More complete feedback (< 10 minutes)
full-check:
runs-on: ubuntu-latest
needs: quick-check
steps:
- uses: actions/checkout@v4
- name: Integration tests
run: make test-integration # ~3 minutes
- name: Build
run: make build # ~2 minutes
- name: Security scan
run: make security-scan # ~1 minute
# Stage 3: Staging deployment (on main)
deploy-staging:
runs-on: ubuntu-latest
needs: full-check
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to staging
run: make deploy-staging
Strategies for keeping CI fast:
1. Parallelize tests:
Run the test suite across multiple goroutines/processes in parallel
100 tests x 100ms = 10 seconds sequential
100 tests x 100ms / 10 parallel = 1 second
2. Test caching:
Only re-run tests affected by the change
Tools: Nx (JavaScript), Bazel, Buck2
3. Separate unit and integration tests:
Unit tests: fast (< 2 minutes), run on every commit
Integration tests: slower (< 10 minutes), run on every PR
E2E tests: slowest, run before releases
4. Fail fast:
If lint or type checking fails, don't run the tests
Save time on problems that are already clear early
Techniques for Maintaining Quality in TBD #
Without long code reviews like in Gitflow, TBD relies on other techniques to maintain quality.
Pair Programming #
Pair Programming in TBD:
→ The driver writes code, the navigator reviews in real time
→ "Review" happens while code is written, not after a PR is created
→ No slow PR reviews — code is already reviewed as it's written
→ Knowledge sharing happens automatically
When pair programming is most effective:
→ Complex or high-risk features
→ New developers onboarding
→ When domain knowledge needs spreading
→ Difficult debugging
The Ship / Show / Ask Framework #
This is a framework popular in TBD for deciding whether each change needs review or not.
Ship — Push straight to the trunk without review:
Good for changes that:
→ Are very small and obvious (typo fixes, minor dependency updates)
→ You're 100% confident about and have test coverage for
→ Don't change user-facing behavior
→ Can be easily reverted if problems arise
Show — Push to the trunk and notify the team (async):
Good for changes that:
→ Are fairly small but involve a technical decision
→ Are refactoring affecting several files
→ You're confident about but want others to know about the change
How: push directly, then post on Slack "hey I just merged X, FYI"
— no approval needed, just information
Ask — Create a PR and request review before merging:
Good for changes that:
→ Involve design or architecture decisions needing discussion
→ Affect many parts of the system
→ You're unsure whether the approach is right
→ Are high-risk changes (payment, security, etc.)
Trunk Health Monitoring #
Metrics to monitor for trunk health:
1. Build success rate
Target: > 99% successful builds
If often red: CI may be flaky or discipline is lacking
2. Time to green
Time from commit to green CI
Target: < 10 minutes
If > 10 minutes: CI needs optimization
3. Time to revert
How long from "trunk broken" to "trunk healthy again"
Target: < 15 minutes
This measures how fast the team responds to broken builds
4. Commit frequency
How many commits per developer per day
Target: 2-5 commits per developer per day
If too low: there may be blockers or branches that are too long
TBD vs Gitflow vs GitHub Flow #
TBD GitHub Flow Gitflow
─────────────────────────────────────────────────────────────────
Main branches 1 (trunk) 1 (main) 2 (main+develop)
Other branches short-lived feature branches feature/release/hotfix
Branch lifespan < 1 day a few days days-weeks
Deployment possible on on PR merge from version tags
every commit to main
Feature isolation Feature Flags Branch isolation Branch isolation
Review process Ship/Show/Ask Mandatory PR Mandatory PR
review review
CI requirement Very strict Strict Moderate
Fit for teams Mature, small Medium Large, enterprise
or large
Versioned releases Less suitable Less suitable Very suitable
Emergency hotfixes Straight to Short feature Hotfix branch
trunk branch
When TBD Fits and When It Doesn’t #
TBD is well suited for:
✓ Teams already mature in engineering — all developers understand
the risks of committing to the trunk and have the discipline
not to merge breaking code
✓ Teams wanting very frequent deployments (several times a day)
✓ Products not requiring versioned releases (SaaS web apps)
✓ Teams with good test coverage and fast CI
✓ Small-to-medium teams with close collaboration and easy communication
TBD is less suited for:
✗ Newly formed teams or still-junior developers —
the risk of "breaking the trunk" is too high
✗ Products requiring multiple version support
(SDKs, libraries, enterprise software with long-term support)
✗ Distributed teams across many timezones without enough overlap
✗ Regulations requiring formal approval before production changes
✗ Teams without adequate test coverage —
TBD without tests is just structured chaos
Google, Facebook, and Microsoft are examples of companies using Trunk-Based Development at large scale. Google uses one large monorepo with TBD for thousands of engineers. What makes this possible is heavy investment in tooling: very fast CI, powerful code search, and a highly disciplined engineering culture. Don’t adopt TBD just because “Google does it” — make sure the foundations (tests, CI, engineering culture) are ready.
Migrating from Gitflow to TBD #
Moving from Gitflow to TBD is a cultural change, not just a technical one. It requires a gradual approach.
flowchart LR
G["Gitflow\n(long-lived branches)"]
GH["GitHub Flow\n(feature branches, PRs)"]
TBDS["TBD with\nshort-lived branches"]
TBDP["Pure TBD\n(direct to trunk)"]
G -->|"Step 1:\nShorten\nbranch lives"| GH
GH -->|"Step 2:\nTighten\nbranch time limits"| TBDS
TBDS -->|"Step 3:\n(optional)\nEliminate\nbranches"| TBDP
style G fill:#E74C3C,color:#fff
style GH fill:#E67E22,color:#fff
style TBDS fill:#F39C12,color:#fff
style TBDP fill:#27AE60,color:#fffA pragmatic migration path:
Step 1 — From Gitflow to GitHub Flow (1-2 months):
→ Remove the develop branch, use only main
→ Feature branches merge straight to main after review
→ Deploy from main on every merge
→ CI must be green before merging
→ Start writing feature flags for long features
Step 2 — From GitHub Flow to TBD with short-lived branches (2-3 months):
→ Limit branch age to at most 1 day
→ Break down PRs: at most 200 lines of changes
→ Invest in test coverage — target: > 70% unit test coverage
→ Optimize CI: target < 10 minutes
Step 3 — Pure TBD (optional, 3-6 months):
→ Start pair programming practice for shared context
→ The Ship/Show/Ask framework to decide when review is needed
→ Invest in proper feature flag management
What must exist before starting the migration:
□ A reliable CI pipeline (success rate > 95%)
□ Adequate test coverage
□ Team alignment — all developers understand and agree
□ Feature flag infrastructure ready (can start simple)
□ Good production monitoring
TBD Anti-Patterns to Avoid #
Committing Straight to the Trunk Without Enough Testing #
// ✗ Anti-pattern: "commit first, test later"
git add .
git commit -m "WIP: add payment feature"
git push origin main
// the trunk is now broken until tests are written
// ✓ Correct TBD: code and tests created together
// If the feature isn't finished → use a feature flag, not a WIP commit
git add src/payment.go src/payment_test.go // code + tests together
git commit -m "Add payment gateway integration (behind feature flag)"
git push origin main
// the trunk stays green because the feature is hidden behind a flag
Leaving the Trunk Broken Too Long #
// ✗ Anti-pattern: trunk broken, developers continue working
Developer A pushes → CI red → Developer A ignores it
Developers B, C, D pull from the broken trunk → all their code sits on broken code
// ✓ TBD rule: a broken trunk = BLOCKING priority for the whole team
When CI is red:
1. The developer who broke it fixes it immediately — this outranks other work
2. If it can't be fixed within 10 minutes → revert the commit
3. The team MUST NOT merge anything to a red trunk
4. A trunk red for more than 15 minutes = an incident to discuss
Feature Flags Never Removed #
// ✗ Anti-pattern: the flag graveyard
if featureFlags.IsEnabled("new_checkout_v1") { /* active since 2021 */ }
if featureFlags.IsEnabled("redesign_checkout_v2") { /* active since 2022 */ }
if featureFlags.IsEnabled("checkout_optimization") { /* active since 2023 */ }
if featureFlags.IsEnabled("new_checkout_final") { /* active since 2024 */ }
// All flags are 100% active but never removed from the code
// The codebase is full of useless conditionals
// ✓ Policy: every flag must have an issue/ticket for cleanup
// After 100% rollout → remove the flag in the next sprint
// Use lint rules to detect expired flags
TBD Without Adequate Test Coverage #
// ✗ Anti-pattern: TBD without a safety net
The team decides to use TBD
Developers commit straight to the trunk
There aren't enough tests
→ Bugs reach production faster than ever!
// TBD without tests isn't TBD — it's chaos
// ✓ Prerequisites before TBD:
Unit test coverage: > 70%
Integration tests for the happy path: present
CI pipeline: < 10 minutes, reliable
Production monitoring: present (fast rollback if a bug slips through)
Trunk-Based Development Checklist #
FOUNDATION (must exist before starting TBD):
□ Reliable CI pipeline (> 95% green, < 10 minutes)
□ Adequate unit test coverage (> 70% for core business logic)
□ Feature flag infrastructure (can start simple with env vars)
□ Production monitoring and alerting (rollback within < 15 minutes)
□ Team alignment — all developers understand and agree with TBD
COMMIT HYGIENE:
□ Every commit contains one small logical change
□ All developers commit to the trunk at least once per day
□ Clear commit messages: what changed and why
□ CI must be green before pushing to the trunk (or fixed immediately)
□ Branches (if any) don't live longer than 1 day
FEATURE FLAGS:
□ All features not production-ready are behind feature flags
□ Every flag registered with an owner and cleanup plan
□ Flags at 100% rollout removed within 1-2 sprints
□ No nested flags (flags inside flags) — too complex
CI/CD:
□ Every commit to the trunk triggers the CI pipeline
□ CI failure is blocking — no merges while the trunk is red
□ Production deployment possible at any time (branch always ready)
□ Rollback procedure defined and executable in < 15 minutes
QUALITY:
□ The Ship/Show/Ask framework used and understood by the team
□ Pair programming for complex or risky features
□ Trunk health metrics monitored (build rate, time to green, commit frequency)
□ Post-mortem if the trunk stays broken for more than 15 minutes
Summary #
- TBD is about integrating more often, not eliminating isolation — an integration problem found 30 minutes after a commit is far cheaper than the same problem found after 2 weeks of branch divergence.
- Feature flags replace branch isolation — merged-but-not-user-ready code is hidden behind flags. This enables early integration without exposing incomplete features.
- CI must be fast for TBD to succeed — CI taking more than 10 minutes makes developers bypass or ignore it. Investing in CI speed is investing in code quality.
- A broken trunk = blocking priority for the whole team — nobody may merge while the trunk is red. This is the most important and most often violated rule during early TBD adoption.
- The Ship/Show/Ask framework replaces “everything needs a PR review” — not every change needs formal review. Small, obvious changes can push directly; changes with architecture decisions need discussion.
- Test coverage is a prerequisite, not a bonus — TBD without tests is structured chaos. Make sure meaningful coverage exists before starting TBD.
- Migrating from Gitflow to TBD must be gradual — Gitflow → GitHub Flow → TBD with short-lived branches → pure TBD. Jumping straight from Gitflow to pure TBD almost always fails.
- Unremoved feature flags become technical debt — every flag is a conditional making code harder to understand. Remove flags within 1-2 sprints after 100% rollout.
- TBD requires a mature engineering culture — this isn’t about tooling, but about trust, discipline, and communication within the team. Teams whose culture isn’t ready will struggle with TBD.
- TBD and Gitflow aren’t always opposites — many large teams use TBD at the team level (committing to a team branch daily), but Gitflow for cross-team coordination and versioned releases. Choose what fits the context, not what sounds coolest.
← Previous: Gitflow