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
mainbranch (trunk) is the only source of truth - the
HEADofmaincommit is the current development version - the
HEADofmaincommit is the current next release version, unless there is an active release branch (RA), in which case it is theHEADof thatrelease-branch. - All changes integrate to
mainfrequently (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
mainfrequently - 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

Diagram shows the three active branch types and their relationships
- The trunk (center) is the single source of truth, giving us the revisionable timeline.
- 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.
- 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:
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:
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).
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:
- Sync with trunk:
git pull origin main- Start with latest changes - Create topic branch:
git checkout -b feature/short-description- Isolate your work - Make small changes: Keep under 400 lines, one logical change per branch
- Integrate regularly: Pull from trunk every few hours to avoid conflicts
- Push and create merge request: Triggers Stage 2 (Pre-commit) and Stage 3 (Merge Request) validation
- Address review feedback: Push additional commits to topic branch
- Squash merge to trunk: One topic branch becomes one trunk commit, branch deleted automatically
Key command:
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:
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":
- Create abstraction layer
- Implement new code behind abstraction
- Gradually migrate calls to new implementation
- All changes integrated to trunk incrementally
- 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:
- Stage 8: Create release branch from trunk (
release/v1.2.0) - Stage 9: Validate in PLTE (automatic) and Demo (exploratively), obtain approval
- Stage 10: Deploy release branch to production
- 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:
- Trunk is always production-ready
- Deploy directly from trunk (no release branches)
- Automated tagging: Commits enumerated and tagged before production deployment (e.g., via scheduled job)
- Fix-forward or rollback if issues occur
- 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
- Fix on trunk first (always the preferred path)
- Create topic branch from trunk
- Implement fix
- Merge to trunk via PR
- Cherry-pick trunk commit to release branch
- 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:
- Create release topic branch from release branch
- Implement minimal fix
- Merge to release branch
- Immediately cherry-pick to trunk
- 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
- Branching Strategies - Detailed flows for RA and CDe patterns
- Unit of Flow - See how trunk fits into the bigger picture
- Deployable Modules - What gets built from trunk
- Implementation Patterns - RA vs CDe decision guidance
- Stages 8-12 - Release stages including feature flags
References
Internal Documentation
External Standards
- Trunk-Based Development (trunkbaseddevelopment.com)
- Conventional Commits - Commit message convention
Additional Resources
Tutorials | How-to Guides | Explanation | Reference
You are here: Explanation — understanding-oriented discussion that clarifies concepts.