Introduction to Git Branching Strategies
Every development team eventually faces a fundamental question: how should we manage our Git branches? The answer isn't one-size-fits-all. Two dominant strategies have emerged over the years—GitFlow and Trunk-Based Development—each with distinct philosophies, workflows, and tradeoffs. Understanding both deeply allows you to choose the right approach for your team's size, release cadence, and risk tolerance.
This tutorial walks you through both strategies from first principles, with hands-on command sequences you can run in your terminal, so you can internalize the patterns and make an informed decision.
What Is GitFlow?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →GitFlow, originally proposed by Vincent Driessen in 2010, is a branching model built around the concept of long-lived branches that serve distinct purposes. It enforces a strict, role-based workflow designed to keep the main branch (called master in the original model, though many teams now use main) in a perpetually releasable state while parallel streams of development happen safely on other branches.
The GitFlow Branch Structure
GitFlow defines the following permanent or semi-permanent branches:
- main (or master) — Holds the production-ready state. Every commit on main represents a release. Tagged with version numbers.
- develop — The integration branch where features are combined and tested before being promoted to main.
- feature/* — Short-lived branches branched off develop where individual features are built. Merged back into develop when complete.
- release/* — Branched off develop when a release is imminent. Used for final polishing, bug fixes, and versioning metadata. Merged into both main and develop.
- hotfix/* — Branched off main to address critical production issues. Merged into both main and develop to ensure the fix propagates forward.
Visualizing GitFlow Step by Step
Let's simulate a complete GitFlow cycle from scratch. Open your terminal and follow along.
Step 1: Initialize the Repository with GitFlow Structure
# Create a new project
mkdir gitflow-demo && cd gitflow-demo
git init
git checkout -b main
# Create an initial commit on main
echo "# GitFlow Demo" > README.md
git add README.md
git commit -m "Initial commit on main"
# Create the develop branch from main
git checkout -b develop
echo "// Development branch initialized" >> README.md
git add README.md
git commit -m "Initialize develop branch"
# Push both branches to remote (if using a remote)
# git remote add origin
# git push -u origin main develop
At this point, main and develop diverge from the same root commit. The develop branch will accumulate all feature work.
Step 2: Creating Feature Branches
Feature branches are always created from develop. Each represents a single, isolated unit of work.
# Ensure you're on develop and it's up to date
git checkout develop
git pull origin develop # if using remote
# Create a feature branch for user authentication
git checkout -b feature/user-auth
# Do some work on the feature
echo "def login(username, password):" > auth.py
echo " # authentication logic" >> auth.py
echo " return True" >> auth.py
git add auth.py
git commit -m "Implement basic login function"
# Simulate a second commit on the same feature
echo "def logout():" >> auth.py
echo " return 'Logged out'" >> auth.py
git add auth.py
git commit -m "Add logout function"
# Push the feature branch
git push -u origin feature/user-auth
Meanwhile, another developer might create a separate feature branch for the dashboard:
# Another developer would run:
git checkout develop
git checkout -b feature/dashboard
echo "def render_dashboard():" > dashboard.py
echo " return 'Dashboard> dashboard.py
git add dashboard.py
git commit -m "Create dashboard rendering function"
git push -u origin feature/dashboard
Step 3: Merging Features into Develop
When a feature is complete, tested, and code-reviewed, it gets merged back into develop—preferably with a merge commit (using --no-ff) to preserve the feature branch history.
# Merge feature/user-auth into develop
git checkout develop
git merge --no-ff feature/user-auth -m "Merge feature/user-auth into develop"
# After merging, you can delete the feature branch
git branch -d feature/user-auth
git push origin --delete feature/user-auth # delete remote branch
# Merge feature/dashboard into develop
git merge --no-ff feature/dashboard -m "Merge feature/dashboard into develop"
git branch -d feature/dashboard
git push origin --delete feature/dashboard
The --no-ff flag forces a merge commit even when a fast-forward merge is possible. This is a GitFlow convention—it keeps the historical record of which commits belonged to which feature, making it easier to revert entire features if needed.
Step 4: Creating a Release Branch
When develop has accumulated enough features for a release, you cut a release branch. This branch is used exclusively for stabilization—no new features are added here, only bug fixes, documentation updates, and version number bumps.
# Cut a release branch from develop
git checkout develop
git checkout -b release/1.0.0
# Bump version numbers, update changelog
echo "1.0.0" > VERSION
echo "## [1.0.0] - 2025-01-15" > CHANGELOG.md
git add VERSION CHANGELOG.md
git commit -m "Bump version to 1.0.0 and update changelog"
# Push the release branch
git push -u origin release/1.0.0
# After QA testing, you might find a minor bug on the release branch
# Fix it directly on release/1.0.0
echo "Fixed edge case in login" >> auth.py
git add auth.py
git commit -m "Fix edge case: empty password should be rejected"
Step 5: Finalizing the Release
When the release branch is deemed ready, you merge it into main (and tag the release), then merge it back into develop so that any bug fixes made on the release branch flow back into ongoing development.
# Merge release branch into main
git checkout main
git merge --no-ff release/1.0.0 -m "Release v1.0.0"
git tag -a v1.0.0 -m "Release version 1.0.0"
# Merge release branch back into develop
git checkout develop
git merge --no-ff release/1.0.0 -m "Merge release/1.0.0 back into develop"
# Delete the release branch
git branch -d release/1.0.0
git push origin --delete release/1.0.0
# Push main, tags, and develop
git push origin main --tags
git push origin develop
Step 6: Handling Hotfixes
A hotfix is an urgent fix for a production issue. It branches directly from main (since main represents what's in production) and, once fixed, is merged into both main and develop.
# A critical bug is discovered in production (v1.0.0)
git checkout main
git checkout -b hotfix/critical-login-fix
# Apply the emergency fix
echo "Security patch applied" >> auth.py
git add auth.py
git commit -m "Patch critical security vulnerability in login"
# Merge hotfix into main and tag it
git checkout main
git merge --no-ff hotfix/critical-login-fix -m "Hotfix for critical login issue"
git tag -a v1.0.1 -m "Hotfix: patch security vulnerability"
# Merge hotfix into develop as well (so future releases include the fix)
git checkout develop
git merge --no-ff hotfix/critical-login-fix -m "Merge hotfix/critical-login-fix into develop"
# Clean up
git branch -d hotfix/critical-login-fix
git push origin --delete hotfix/critical-login-fix
git push origin main --tags develop
GitFlow Summary Commands Cheat Sheet
# Start a feature
git checkout -b feature/xyz develop
# Finish a feature
git checkout develop
git merge --no-ff feature/xyz
git branch -d feature/xyz
# Start a release
git checkout -b release/1.x develop
# Finish a release
git checkout main
git merge --no-ff release/1.x
git tag -a v1.x
git checkout develop
git merge --no-ff release/1.x
git branch -d release/1.x
# Start a hotfix
git checkout -b hotfix/urgent main
# Finish a hotfix
git checkout main
git merge --no-ff hotfix/urgent
git tag -a v1.x.y
git checkout develop
git merge --no-ff hotfix/urgent
git branch -d hotfix/urgent
What Is Trunk-Based Development?
Trunk-Based Development (TBD) takes the opposite approach: instead of multiple long-lived branches, developers integrate their changes directly into a single shared branch—the trunk (typically main or master)—at least once every 24 hours. Short-lived feature branches may exist, but they are kept extremely small (often lasting only hours, never more than a day or two) and are merged directly into the trunk without intermediate integration branches like develop or release.
The core philosophy is: if you integrate frequently, you minimize merge conflicts and always have a releasable trunk. Releases are achieved either by branching from the trunk at the moment of release (release branching) or by using feature flags to decouple deployment from release.
The Trunk-Based Development Branch Structure
- main (trunk) — The single source of truth. All developers push directly to main via short-lived branches or (in small teams) even direct commits. The trunk must always be green and deployable.
- Short-lived feature branches — Optional. Branched from main, live for at most 1-2 days, merged back to main via pull request. Often named after the developer or ticket ID.
- Release branches — Optional. Some TBD teams branch from main at release time for stabilization. Others skip release branches entirely and rely on feature flags to toggle incomplete features in production.
Visualizing Trunk-Based Development Step by Step
Let's simulate a TBD workflow. We'll use short-lived feature branches (the most common TBD variant for teams with code review requirements).
Step 1: Initialize the Repository
mkdir tbd-demo && cd tbd-demo
git init
git checkout -b main
echo "# Trunk-Based Development Demo" > README.md
git add README.md
git commit -m "Initial commit on trunk (main)"
git push -u origin main # if using remote
Notice there is no develop branch. main is the trunk—the single integration point for all work.
Step 2: Create a Short-Lived Feature Branch
In TBD, branches are extremely short-lived. A developer grabs a ticket, creates a branch off the latest main, makes changes, and opens a pull request—all ideally within the same day.
# Pull the latest trunk to ensure you're current
git checkout main
git pull origin main
# Create a short-lived branch for a small task
git checkout -b task/add-login-endpoint
# Make focused, incremental changes
echo "POST /api/login -> returns JWT token" > endpoints.md
git add endpoints.md
git commit -m "Document login endpoint specification"
# Implement the actual endpoint
mkdir src
echo "from flask import Flask, request" > src/app.py
echo "app = Flask(__name__)" >> src/app.py
echo "@app.route('/api/login', methods=['POST'])" >> src/app.py
echo "def login():" >> src/app.py
echo " return {'token': 'jwt-placeholder'}" >> src/app.py
git add src/app.py
git commit -m "Implement login endpoint with placeholder JWT"
# Push the branch and open a PR immediately
git push -u origin task/add-login-endpoint
Step 3: Merge to Trunk Quickly
After code review and CI passes, merge the branch into main and delete the branch. In TBD, the emphasis is on fast integration—don't let branches linger.
# Switch to main, pull latest (in case someone else merged)
git checkout main
git pull origin main
# Merge the short-lived branch
git merge --no-ff task/add-login-endpoint -m "Merge task/add-login-endpoint"
# Delete the branch locally and remotely
git branch -d task/add-login-endpoint
git push origin --delete task/add-login-endpoint
# Push the updated trunk
git push origin main
Some TBD teams prefer squash merging to keep a linear, clean history on main:
# Alternative: squash merge for a clean trunk history
git checkout main
git merge --squash task/add-login-endpoint
git commit -m "Implement login endpoint with placeholder JWT"
git branch -D task/add-login-endpoint
git push origin main
Step 4: Continuous Integration on the Trunk
In TBD, the trunk must never be broken. Every commit to main should pass CI. If a commit breaks the build, fixing it takes priority over all other work.
# Example CI pipeline trigger (conceptual)
# On every push to main, your CI system runs:
# 1. Lint
# 2. Unit tests
# 3. Integration tests
# 4. Build artifact
# If something breaks, the offending developer immediately:
git checkout main
git pull origin main
git checkout -b hotfix/fix-broken-build
# Fix the issue
echo "Fixed import error" >> src/app.py
git add src/app.py
git commit -m "Fix: correct broken import in app.py"
git push -u origin hotfix/fix-broken-build
# Merge the fix with priority
git checkout main
git merge --no-ff hotfix/fix-broken-build -m "Hotfix: repair broken CI build"
git push origin main
git branch -d hotfix/fix-broken-build
Step 5: Preparing a Release in TBD
There are two common approaches for releases in TBD:
Approach A: Release Branching — Cut a release branch from the trunk at the moment you decide to ship. Only critical bug fixes go onto the release branch. All normal development continues on the trunk uninterrupted.
# Cut a release branch from the current state of main
git checkout main
git checkout -b release/2.0.0
# Stabilization work happens on the release branch
echo "Updated version to 2.0.0" >> README.md
git add README.md
git commit -m "Set version 2.0.0 for release"
git push -u origin release/2.0.0
# Meanwhile, other developers keep pushing to main
# The release branch receives only critical fixes:
git checkout release/2.0.0
echo "Critical fix for release" >> src/app.py
git add src/app.py
git commit -m "Fix critical memory leak before release"
# When ready, tag the release and merge back to main
git checkout main
git merge --no-ff release/2.0.0 -m "Merge release/2.0.0 stabilization fixes"
git tag -a v2.0.0 -m "Release version 2.0.0"
git push origin main --tags
git branch -d release/2.0.0
git push origin --delete release/2.0.0
Approach B: Feature Flags (No Release Branches) — Instead of branching for release, teams use feature flags to hide incomplete features in production. The trunk is always deployable. Releasing becomes a configuration change, not a code merge.
# Conceptual feature flag implementation
# File: src/feature_flags.py
FEATURE_FLAGS = {
"new_dashboard": False, # toggle in production config
"beta_search": False,
}
def is_enabled(flag_name, user_context=None):
return FEATURE_FLAGS.get(flag_name, False)
# In your application code:
if is_enabled("new_dashboard"):
render_new_dashboard()
else:
render_old_dashboard()
# To release "new_dashboard", just flip the flag in config:
# FEATURE_FLAGS["new_dashboard"] = True
# No branch, no merge, no deployment — just configuration
This approach requires infrastructure for feature flag management (tools like LaunchDarkly, Unleash, or simple config files) and discipline around cleaning up flags after release.
GitFlow vs. Trunk-Based Development: A Detailed Comparison
Both strategies are valid, but they optimize for different priorities. Here's how they stack up across key dimensions:
Branch Complexity
- GitFlow: High. Multiple long-lived branches (main, develop, release, hotfix) plus feature branches. Developers must understand which branch to target at each stage.
- Trunk-Based: Low. One primary branch (main). Short-lived feature branches are optional and transient. The mental model is simple: everything flows to main.
Integration Frequency
- GitFlow: Low to medium. Features may live on separate branches for days or weeks before being merged into develop. The develop branch itself accumulates changes until a release branch is cut.
- Trunk-Based: Very high. Developers integrate into main at least daily, often multiple times per day. This minimizes the "integration gap" and reduces merge conflicts.
Release Cadence
- GitFlow: Suited for planned, periodic releases (e.g., weekly, biweekly, monthly). The release branch provides a stabilization buffer.
- Trunk-Based: Enables continuous delivery. The trunk is always releasable. Teams can deploy multiple times per day if desired. Release branches are optional and short-lived.
Merge Conflict Surface Area
- GitFlow: Larger. Because branches diverge for longer periods, the probability and complexity of merge conflicts increases. Merging a weeks-old feature into develop can be painful.
- Trunk-Based: Smaller. Frequent integration means changes are small and conflicts are rare. When conflicts occur, they're trivial to resolve because the scope is tiny.
Code Review Workflow
- GitFlow: Pull requests target develop (or release branches). Reviews can happen asynchronously over days. Large PRs are common.
- Trunk-Based: Pull requests target main. Reviews must happen quickly because branches are short-lived. This encourages small, focused PRs that are easy to review thoroughly.
Hotfix Workflow
- GitFlow: Formal hotfix branches from main, merged to both main and develop. Clear protocol.
- Trunk-Based: Hotfixes are just regular fixes on main (or on a release branch if one exists). If the trunk is always deployable, you simply push the fix to main and deploy. No separate hotfix protocol needed.
Team Size Suitability
- GitFlow: Works well for medium-to-large teams with formal release processes, especially in environments with longer QA cycles or regulatory requirements.
- Trunk-Based: Scales from solo developers to large organizations (Google, Facebook use variants of TBD). Requires strong CI/CD infrastructure and discipline around keeping the trunk green.
When to Use GitFlow
GitFlow shines in these scenarios:
- Products with versioned releases (e.g., desktop software, on-premise enterprise software, mobile apps with store submission cycles). The release branch provides a natural stabilization phase.
- Teams new to continuous delivery who need explicit stages and formal handoffs between development, QA, and release.
- Regulated industries (finance, healthcare, aerospace) where traceability of changes through formal stages is required for compliance.
- Open source projects with many external contributors. GitFlow's structured branch roles help maintainers manage contributions from unknown developers.
- Long-running feature development where features genuinely cannot be broken into small increments (though this is often a sign of architecture problems).
When to Use Trunk-Based Development
TBD excels in these contexts:
- SaaS and web applications with continuous deployment pipelines. The ability to push to production multiple times a day is a competitive advantage.
- High-performing DevOps teams practicing continuous delivery. TBD is the branching strategy recommended by the DORA research program for elite-performing teams.
- Microservice architectures where each service has its own repository. Small, focused repos benefit from the simplicity of trunk-based workflows.
- Teams practicing pair or ensemble programming where code is written together and integrated immediately.
- Startups and fast-moving teams where speed of iteration outweighs the need for formal release processes.
Best Practices for GitFlow
- Use
--no-ffmerges consistently. This preserves the feature branch topology and makes reverting entire features straightforward. - Keep feature branches focused. A feature branch should address one concern. If you find yourself working on multiple unrelated things, split them into separate branches.
- Don't merge feature branches into other feature branches. This creates hidden dependencies and complicates the merge back to develop.
- Tag every release on main. Tags provide immutable reference points for production deployments.
- Clean up branches after merging. Delete feature, release, and hotfix branches once they're merged. A cluttered branch list confuses the team.
- Automate the workflow. Use GitFlow AVH tools or scripts to reduce manual branch management errors.
# Install git-flow AVH (a popular GitFlow automation tool)
# On macOS:
brew install git-flow-avh
# Initialize GitFlow in your repo
git flow init
# Start a feature (automatically branches from develop)
git flow feature start my-feature
# Finish a feature (automatically merges to develop, deletes branch)
git flow feature finish my-feature
# Start a release
git flow release start 1.0.0
# Finish a release (merges to main and develop, tags, deletes branch)
git flow release finish 1.0.0
# Start a hotfix
git flow hotfix start critical-bug
# Finish a hotfix (merges to main and develop, tags, deletes branch)
git flow hotfix finish critical-bug
Best Practices for Trunk-Based Development
- Integrate at least once per day. The defining practice of TBD. If you can't merge within 24 hours, your branch is too big—split it further.
- Keep branches extremely short-lived. A branch lasting more than two days is a red flag. Break work into smaller, mergeable chunks.
- Invest heavily in CI/CD. Fast, reliable CI with comprehensive tests is non-negotiable. Every commit to main must be verified automatically.
- Use feature flags for incomplete work. Never commit half-finished features to main without hiding them behind a flag. The trunk must always be deployable.
- Practice synchronous code review. Since branches are short-lived, reviews must happen promptly. Consider pair programming for real-time review.
- Build a robust rollback mechanism. If the trunk is always deployable but a bad change slips through, you need the ability to roll back instantly.
- Consider squash merging for a linear history. Many TBD teams squash commits on merge to keep the trunk history clean and bisectable.
# A typical TBD daily workflow with squash merging
# Morning: pull latest trunk
git checkout main
git pull origin main
# Create a tiny branch for a single task
git checkout -b task/improve-error-handling
# Make focused changes
echo "try: import optional_dependency" > error_handling.py
echo "except ImportError: fallback()" >> error_handling.py
git add error_handling.py
git commit -m "Add graceful fallback for missing dependency"
# Push and open PR immediately
git push -u origin task/improve-error-handling
# After review approval, squash merge to main
git checkout main
git pull origin main
git merge --squash task/improve-error-handling
git commit -m "Add graceful fallback for missing dependency (#42)"
git push origin main
# Delete the branch
git branch -D task/improve-error-handling
git push origin --delete task/improve-error-handling
# Deploy immediately if CI is green
# (deployment pipeline triggers automatically on push to main)
Hybrid Approaches
Many teams evolve a hybrid strategy that borrows from both models:
- GitHub Flow: A simplified GitFlow with only main and feature branches. Features merge directly to main, which is always deployable. Essentially TBD with a pull request workflow.
- GitLab Flow: Uses environment branches (e.g., staging, production) that track the trunk at different cadences, combined with feature branches merging directly to main.
- Release train model: Teams cut a release branch on a fixed schedule (e.g., every two weeks), but all development happens on short-lived branches merged to main. The release branch is just a snapshot.
Common Pitfalls to Avoid
GitFlow Pitfalls
- Overly long-lived feature branches that accumulate dozens of commits before merging to develop. This defeats the purpose of continuous integration.
- Neglecting to merge release branches back to develop, causing bug fixes to be lost in future releases.
- Using GitFlow for simple projects where the overhead outweighs the benefits. A five-person team building a simple web app probably doesn't need GitFlow.
Trunk-Based Development Pitfalls
- Committing incomplete features without feature flags, breaking the deployability of the trunk.
- Insufficient CI coverage leading to frequent trunk breakages and frustrated teams.
- Branches that live too long because work isn't decomposed properly. If you consistently have branches lasting a week, you're not actually doing TBD.
Migrating from GitFlow to Trunk-Based Development
If your team is on GitFlow and wants to move toward TBD, don't flip a switch overnight. Here's a gradual migration path:
# Phase 1: Start merging features to main more frequently
# Instead of accumulating features on develop for weeks,
# merge smaller chunks directly to main behind feature flags.
# Phase 2: Deprecate the develop branch
# Begin treating main as the integration point.
# Feature branches now branch from and merge to main.
# Phase 3: Shorten branch lifetimes
# Set a team goal: no branch lives more than 2 days.
# Break large features into smaller, flag-guarded increments.
# Phase 4: Implement feature flags
# Introduce a flag system so incomplete work can live safely on main.
# Example flag configuration:
cat > flags.config.json << 'EOF'
{
"new_checkout_flow": false,
"ai_recommendations": false,
"dark_mode": false
}
EOF
# Phase 5: Invest in CI speed and reliability
# Ensure the trunk build completes in under 10 minutes.
# Make fixing a broken trunk the team's highest priority.
# Phase 6: Eliminate release branches (optional)
# Move to flag-based releases where possible.
# Tag main directly for versioned releases.
Conclusion
GitFlow and Trunk-Based Development represent two ends of a spectrum, not two mutually exclusive camps. GitFlow offers structure, traceability, and clear separation of concerns—qualities that shine in environments with formal release processes, regulatory requirements, or distributed open-source collaboration. Trunk-Based Development prioritizes speed, simplicity, and continuous integration, making it the natural choice for teams practicing continuous delivery on web-scale applications.
The best strategy is the one your team can execute reliably. If you're consistently fighting merge conflicts and struggling with long-running branches, move toward the trunk. If you're shipping desktop software with six-month release cycles and need formal stabilization phases, GitFlow's structure will serve you well. Many successful teams land somewhere in between—using short-lived branches merged to a single trunk, with optional release branches for stabilization, and feature flags to decouple deployment from release.
Whatever strategy you choose, the fundamental principle remains: integrate early and often. The longer your changes live in isolation, the more painful the eventual merge will be. The branching strategy is just a tool—what matters is the engineering discipline behind it.