← Back to DevBytes

Git Branching Strategies: GitFlow vs Trunk-Based Development

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:

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

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

Integration Frequency

Release Cadence

Merge Conflict Surface Area

Code Review Workflow

Hotfix Workflow

Team Size Suitability

When to Use GitFlow

GitFlow shines in these scenarios:

When to Use Trunk-Based Development

TBD excels in these contexts:

Best Practices for GitFlow

# 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

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

Common Pitfalls to Avoid

GitFlow Pitfalls

Trunk-Based Development Pitfalls

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles