Skip to content

Trunk-Based Development

Introduction

Trunk-Based Development (TBD) is the branching strategy that enables Continuous Integration and Continuous Delivery.

Instead of long-lived topic branches that defers integration, TBD emphasizes frequent integration to a single main branch (trunk), enabling rapid feedback and reducing merge conflicts.

This approach is fundamental to achieving:

  • Continuous Integration: All developers integrate to trunk at least daily
  • Continuous Delivery: Head of Trunk is always in a releasable state
  • Fast feedback: Issues detected within seconds to minutes, not weeks to months
  • Reduced risk: Small, incremental changes are easier to validate and rollback

Integration with CD Model

Trunk-Based Development directly supports several CD Model stages:

  • Stage 1 (Authoring): Work on short-lived topic branches
  • Stage 3 (Merge Request): Rebase downstream and Squash-merge topic branches upstream to trunk
  • Stage 4 (Commit): Trunk commits always trigger full validation
  • Stage 8 (Start Release): Create release branches from trunk (RA pattern)
  • Stage 12 (Release Toggling): Feature flags enable trunk-based development for incomplete features

See CD Model Overview for the complete 12-stage framework.


Core Principles

1. Single Source of Truth

Principle: There is only ever one meaningful version of the code: the current one.

main is always in a releasable state:

  • The main branch (trunk) is the only source of truth
  • the HEAD of main commit is the current development version
  • the HEAD of main commit is the current next release version, unless there is an active release branch (RA), in which case it is the HEAD of that release- branch.
  • All changes integrate to main frequently (at least daily)

Why this matters:

  • Eliminates confusion about which version is next release candidate
  • Enables continuous delivery from trunk
  • Reduces integration debt
  • Provides clear traceability

Anti-pattern to avoid: Long-lived feature branches that become "alternative versions" of truth, causing painful merges and integration delays.

2. Do Not Branch (Or Branch Very Briefly)

Principle: At any given time, there are only 3 active branch types: trunk, topic branches, and release branches.

  • Topic branches live for hours or at most 1-2 days
  • Changes are kept small and focused
  • Branches are deleted immediately after squash merging

Why this matters:

  • Only ever one timeline (+ release branches for RA) to revision.
  • Prevents integration drift
  • Enables continuous integration
  • Reduces merge conflicts
  • Faster feedback loops

Anti-pattern to avoid: "Feature branches" that live for weeks, accumulating hundreds of changes and causing massive merge conflicts.

3. Small, Incremental Changes

Principle: Work in small batches that are continuously integrated.

  • Each change is a small, logical increment
  • Large features broken into hidden, but deployable, pieces
  • Feature hiding used for incomplete features
  • Feature flagging used to demo or a-b test or release features

Why this matters:

  • Easier to review (< 400 lines)
  • Faster to validate (Stage 2-6)
  • Lower risk (small changes are safer)
  • Enables continuous delivery

Anti-pattern to avoid: Massive "big bang" merges with thousands of lines, making review impossible and rollback risky.

4. Continuous Integration to Trunk

Principle: Integrate at least daily, preferably multiple times per day.

  • Developers pull from main frequently
  • Push changes as soon as they pass local validation
  • Pipeline validates every trunk commit

Why this matters:

  • Early detection of integration issues
  • Prevents merge conflicts
  • Maintains releasable trunk
  • Supports rapid delivery

Anti-pattern to avoid: Developers working in isolation for days/weeks before attempting integration.

5. Branch by abstraction

Principle: Instead of branching out for a feature, make an abstraction in the production code that can be toggled off (hidden)

  • Developers learn how to branch by abstration, as an alternative to feature branching

Why this matters:

  • The Branch by abstraction tecnique is a key enabler for the developer to work with continuous integration.
  • Many developers are schooled to always hide new features in branches and see the point of integration as the merging of that big branch
  • Developers will fall back to old learnings, when the delay in CI becomes to big

Anti-pattern to avoid: Developers working in isolation for days/weeks before attempting integration.


Branch Types and Flow

Branching Overview

Diagram shows the three active branch types and their relationships

  1. The trunk (center) is the single source of truth, giving us the revisionable timeline.
  2. Topic branches (below trunk in illustration) branch from trunk or release branches for short-lived development work. Changes on topic branches are squash-merged back, resulting in one topic branch creating exactly one commit on the destination branch. Topic branches also comes in a variant where they branch off of a release branch, in which case we call them Release Topic branches. Topic branches can also become spikes (experimental work) that never progress past the Merge Request stage.
  3. Release branches (above trunk in illustration) branch from trunk at Stage 8 for the Release Approval pattern. All paths to trunk and release branches go through merge requests, never direct commits.

Trunk Branch (Main)

Definition: The single branch representing the trunk.

Characteristics:

  • Always deployable
  • Protected - no direct commits
  • All changes via merge request
  • Updated multiple times per day
  • Only meaningful version of the code

Access:

  • Read: All developers
  • Write: Via approved merge requests only

Purpose:

  • Single source of truth
  • Continuous integration point
  • Release candidate source

Topic Branches

Definition: Short-lived branches for individual changes.

Characteristics:

  • Branched from trunk (or occasionally release branch)
  • Live for hours to maximum 2 days
  • Squash-merged to preserve clean history
  • Deleted immediately after merge
  • Temporary, local development scope

One topic branch = One trunk commit after squash merge:

Naming Convention:

feature/short-description
fix/bug-description
docs/documentation-update
refactor/improve-component

Note: The authoritative branch naming policy specifies topic/[user]/[topic-moniker] (e.g., topic/ausr/fix-build), but some teams prefer the semantic prefixes shown above. This project uses semantic prefixes. Only trunk (main) and release (release/[x]) branch names are strictly enforced.

Purpose:

  • Isolate work in progress
  • Enable code review via merge request
  • Provide temporary workspace
  • NOT for hiding incomplete features (use feature flags)

Topic branches are NOT feature branches:

  • Feature branches live for weeks/months
  • Topic branches live for hours/days
  • Feature branches accumulate many commits
  • Topic branches result in single squash commit

Release Branches

Definition: Short branches isolating releases.

Characteristics:

  • Created from trunk at Stage 8 (Start Release)
  • Short-lived (days to weeks)
  • Only critical fixes allowed
  • Fixes cherry-picked back to trunk
  • Used in Release Approval (RA) pattern, not Continuous Deployment (CDe)

Naming Convention:

release/1         # First release
release/2         # Second release
release/10        # Tenth release
release/product-name/5   # Fifth release of product-name (mono repo)

Note: The [x] is an incremental integer release number, NOT a semantic version number. It can be part of a version number (e.g., release 10 might be tagged as v1.2.0), but the branch name uses the release count, not the version.

Purpose:

  • Isolate release for validation
  • Allow trunk to continue evolving
  • Apply critical fixes without trunk changes
  • Maintain stable release candidate

See Implementation Patterns for RA vs CDe details and Branching Strategies for detailed flows.


Commits

In trunk-based development, a commit refers specifically to commits made to trunk or release branches, not local commits on topic branches.

Key Principles:

  • Local commits on topic branches are temporary and have no lasting git history
  • Only squash-merged commits to trunk/release branches are lasting
  • Trunk and release branch history are straight lines
  • Each topic branch integration = one commit

Commit Format:

Commits follow semantic commit conventions.

Commit format for poly repo:

type(scope): short description

Longer description if needed.

Related to #123

Commit format for mono repo, affecting multiple modules:

multi-module: type(scope): short summary

(...)

module_a: type(scope): short description

Longer description if needed.

Related to #123

(...)

module_b: type(scope): short description

Longer description if needed.

Related to #321

Commit format for mono repo, affecting single module:

This will just fallback to same format as used for single module repositories (poly).

type(scope): short description

Longer description if needed.

Related to #123

Types: feat, fix, docs, refactor, test, chore

Why Squash Merge:

  • Clean, linear history
  • One commit per logical change
  • Easy to revert
  • Clear traceability
  • Simplifies cherry-picking

Example:

Topic branch (local):
  - WIP: start feature
  - fix typo
  - address review comments
  - final cleanup

Trunk (after squash merge):
  - feat(api): add user authentication endpoint

Daily Development Flow

Typical workflow:

  1. Sync with trunk: git pull origin main - Start with latest changes
  2. Create topic branch: git checkout -b feature/short-description - Isolate your work
  3. Make small changes: Keep under 400 lines, one logical change per branch
  4. Integrate regularly: Pull from trunk every few hours to avoid conflicts
  5. Push and create merge request: Triggers Stage 2 (Pre-commit) and Stage 3 (Merge Request) validation
  6. Address review feedback: Push additional commits to topic branch
  7. Squash merge to trunk: One topic branch becomes one trunk commit, branch deleted automatically

Key command:

gh pr merge --squash --delete-branch

This squash-merges your topic branch to trunk, triggering Stage 4 (Commit) validation, and automatically deletes the topic branch.


Feature Hiding

The Problem: Incomplete Features in Trunk

When practicing Continuous Integration with small batches, you'll often integrate partial features into trunk.

These incomplete features must not affect production behavior.

You cannot rely on branches to hide incomplete work - the work is in trunk, potentially deployed to production.

Feature Hiding Strategies

Level 1: Code-Level Hiding:

Hide new code by simply not activating it:

// New implementation exists but not injected
type NewPaymentProcessor struct {}

// Still using old implementation
func main() {
    processor := OldPaymentProcessor{} // Not injecting new one yet
}

Benefits: Simple, no infrastructure needed Limitation: Requires deployment to activate

Level 2: Configuration-Based Activation:

Use configuration to control feature activation:

# config.yaml
features:
  newCheckoutFlow: false  # Deploy OFF, flip to true later

Benefits: Activation decoupled from deployment Limitation: Requires redeployment to change config

Level 3: Feature Flags:

Use runtime feature flags for full control:

if featureFlags.IsEnabled("new-checkout-flow", user) {
    return NewCheckoutFlow(user)
} else {
    return OldCheckoutFlow(user)
}

Benefits:

  • Deployment fully decoupled from release
  • Enable for percentage of users (gradual rollout)
  • Instant rollback without redeployment
  • A/B testing capabilities

This is Stage 12 (Release Toggling) of the CD Model.

See Stages 8-12 for feature flag implementation patterns.

When to Use Each Strategy

Strategy Use When Don't Use When
Code-Level Simple internal changes, refactoring Larger feature sets (complexity not worth it)
Configuration Features ready for immediate activation Need gradual rollout or A/B testing
Feature Flags Customer-facing features, risky changes, gradual rollouts Simple internal changes (overhead not worth it)

Branch by Abstraction

For larger or architectural changes, use "branch by abstraction":

  1. Create abstraction layer
  2. Implement new code behind abstraction
  3. Gradually migrate calls to new implementation
  4. All changes integrated to trunk incrementally
  5. Remove old implementation when migration complete

This avoids long-lived branches for these types of changes.


Release Flows and Branching

The branching strategy differs based on your release flow.

Release Approval (RA) Pattern

For regulated systems requiring formal change approval:

  1. Stage 8: Create release branch from trunk (release/v1.2.0)
  2. Stage 9: Validate in PLTE (automatic) and Demo (exploratively), obtain approval
  3. Stage 10: Deploy release branch to production
  4. Hotfixes: Fix on main via single commit (via PR) and cherry pick that to release branch via another PR (Recommended). Alternatively: Create release topic branch (from release), merge via PR, cherry-pick back to trunk via another PR (Not recommended, but sometimes neccessary)

Branch Lifecycle:

  • Release branch lives until release is superseded
  • Only critical fixes allowed on release branch
  • All fixes MUST be cherry-picked back to trunk (avoid regressions)

See Implementation Patterns and Branching Strategies for details.

Continuous Deployment (CDe) Pattern

For non-regulated systems with high confidence:

  1. Trunk is always production-ready
  2. Deploy directly from trunk (no release branches)
  3. Automated tagging: Commits enumerated and tagged before production deployment (e.g., via scheduled job)
  4. Fix-forward or rollback if issues occur
  5. Feature flags provide control instead of branches

No release branches needed - HEAD of trunk IS the release candidate. A deployment mechanism (pipeline or scheduled job) enumerates, tags, and deploys trunk commits to production automatically.

See Implementation Patterns for details.


Cherry-Picking Fixes

What is Cherry-Picking?

Cherry-picking copies a commit, or series of commits, from one branch to another, creating a new squashed commit (via PR) with the same changes.

Primary use case: Bringing fixes from trunk to release branches.

When to Cherry-Pick

Scenario: Critical bug found after release branch created

  1. Fix on trunk first (always the preferred path)
  2. Create topic branch from trunk
  3. Implement fix
  4. Merge to trunk via PR
  5. Cherry-pick trunk commit to release branch
  6. Create PR for release branch with cherry-picked commit

Why fix on trunk first?

  • Ensures fix is in next release
  • Avoids regressions
  • Maintains trunk as single source of truth

Emergency: Fixing Directly on Release Branch

Only when trunk has diverged significantly and fix is urgent:

  1. Create release topic branch from release branch
  2. Implement minimal fix
  3. Merge to release branch
  4. Immediately cherry-pick to trunk
  5. Verify fix works in both branches

⚠️ Warning: Fixing on release branch first risks forgetting to cherry-pick to trunk, causing regressions.

Emergency Fixes in Production

Emergency Fixes in Production follows the exact same procedure as any other change.

This is a fundamental constraint of the model: The validated path to production is used for all production changes

Git Commands

# On trunk, after merging fix via PR
git log  # Find commit SHA of the fix

# Switch to release branch
git checkout release/v1.2.0

# Cherry-pick the fix commit
git cherry-pick <commit-sha-from-trunk>

# Create PR with cherry-picked commit
git push origin release/v1.2.0

# Via GitHub CLI
gh pr create --base release/v1.2.0 --title "Cherry-pick: Fix critical bug"

Best Practices

Key guidelines for effective trunk-based development workflows.

Stage Duration Guidelines

If these stages take longer than indicated, your changes are too large - break them down:

  • Stage 2 (Pre-commit): < 10 minutes (L0/L1 tests, linting, secret scanning)
  • Stage 3 (Merge Request): < 30 minutes (Stage 2 + L2 tests + peer review)
  • Stage 4 (Commit): < 30 minutes (all previous + Hybrid E2E + artifact build)

Conflict Resolution

Prevention is key: Pull from trunk frequently (every few hours), keep changes small, and integrate hourly to daily. When conflicts occur: pull latest, resolve locally. Remember to rebase main on to your local branch if complex merge.


Next Steps

References

Internal Documentation

External Standards

Additional Resources


Tutorials | How-to Guides | Explanation | Reference

You are here: Explanation — understanding-oriented discussion that clarifies concepts.