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:#fff

Principle 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:#fff
Short-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:#fff
A 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

  Next: Pull Request Fundamental →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact