Git Flow: The Original Branching Strategy Explained
Master the Git Flow branching model with master, develop, feature, release, and hotfix branches. Learn when to use it, common pitfalls, and production best practices.
Git Flow: The Original Branching Strategy Explained
Git Flow is the branching model that started it all. Created by Vincent Driessen in 2010, it introduced a structured approach to managing parallel development streams that became the default choice for teams worldwide for over a decade.
The model defines five distinct branch types, each with a specific purpose and lifecycle. It assumes a project with scheduled releases, where features are developed independently, tested together, and shipped on a predictable cadence. Understanding Git Flow is essential — even if you never use it, every modern branching strategy exists as a reaction to it.
This post covers the complete Git Flow model, when it makes sense, when it doesn’t, and how to avoid the traps that have derailed countless teams.
When to Use / When Not to Use
Use Git Flow When
- Scheduled releases — Your team ships on a calendar cadence (monthly, quarterly) rather than continuously
- Multiple versions in production — You need to maintain v1.x while developing v2.x simultaneously
- Large teams with defined roles — Separate developers, QA, and release managers benefit from the structure
- Enterprise or regulated environments — Audit trails, change management, and release gates are mandatory
- Desktop or mobile applications — Where each release requires a build, sign, and distribution process
Do Not Use Git Flow When
- Continuous deployment — If you deploy on every merge, the release branch overhead is wasted
- Small teams (1-5 people) — The ceremony outweighs the coordination benefit
- Web services and SaaS — Where “release” means pushing to production, not packaging a binary
- Fast-moving startups — Where the time from feature-complete to production should be minutes, not days
Core Concepts
Git Flow defines five permanent and temporary branch types:
| Branch | Prefix | Purpose | Lifetime |
|---|---|---|---|
main | — | Production-ready code only | Permanent |
develop | — | Integration branch for features | Permanent |
feature/* | feature/ | New features and enhancements | Temporary |
release/* | release/ | Preparation for a production release | Temporary |
hotfix/* | hotfix/ | Emergency fixes to production | Temporary |
The two permanent branches form the backbone: main holds only production-ready code, while develop serves as the integration branch where completed features merge before release.
graph LR
A[main] --> B[develop]
B --> C[feature/login]
B --> D[feature/search]
C --> B
D --> B
B --> E[release/1.0]
E --> A
E --> B
A --> F[hotfix/1.0.1]
F --> A
F --> B
Architecture and Flow Diagram
The complete Git Flow lifecycle from feature inception through production release:
graph TD
A[main - Production] -->|fork| B[develop - Integration]
B -->|fork| C[feature/user-auth]
B -->|fork| D[feature/api-v2]
C -->|merge| B
D -->|merge| B
B -->|fork| E[release/2.0]
E -->|bug fixes| E
E -->|merge + tag| A
E -->|merge| B
A -->|fork| F[hotfix/security-patch]
F -->|merge + tag| A
F -->|merge| B
Step-by-Step Guide
1. Initialize Git Flow
Most teams use the git-flow CLI extension or follow the manual workflow:
# Initialize with defaults
git flow init
# This creates develop from main and sets up branch prefixes
# Default prefixes: feature/, release/, hotfix/, support/
2. Develop a Feature
Features branch from develop and merge back into develop:
# Start a new feature
git flow feature start user-authentication
# This creates and checks out feature/user-authentication from develop
# Work on the feature...
git add .
git commit -m "feat: implement OAuth2 login flow"
# Finish the feature (merges to develop and deletes the branch)
git flow feature finish user-authentication
Manual equivalent:
git checkout develop
git checkout -b feature/user-authentication
# ... work ...
git commit -am "feat: implement OAuth2 login flow"
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authentication
The --no-ff flag is critical — it preserves the feature branch topology in history even after the merge.
3. Prepare a Release
When develop has enough features for a release:
# Start a release branch
git flow release start 2.0.0
# This creates release/2.0.0 from develop
# Bump version numbers, fix last-minute bugs
git commit -am "chore: bump version to 2.0.0"
# Finish the release (merges to main AND develop, tags main)
git flow release finish 2.0.0
git push origin main develop --tags
The release branch is the only branch that merges to both main and develop. This ensures:
maingets the tagged releasedevelopgets any release-only bug fixes
4. Handle a Hotfix
When production breaks, you don’t wait for the next release:
# Start a hotfix from main
git flow hotfix start 2.0.1
# Fix the bug
git commit -am "fix: resolve null pointer in payment processing"
# Finish (merges to main AND develop, tags main)
git flow hotfix finish 2.0.1
git push origin main develop --tags
Hotfixes bypass develop entirely because the fix must go to production immediately. The merge back to develop prevents the bug from reappearing in the next release.
Production Failure Scenarios + Mitigations
| Scenario | What Happens | Mitigation |
|---|---|---|
| Release branch drifts | release/* lives for weeks, accumulating fixes that never reach develop | Time-box release branches to 1-2 weeks max; merge develop into release daily |
| Hotfix merge conflicts | Hotfix merges cleanly to main but conflicts with develop | Keep develop close to main; run integration tests on hotfix before merging to develop |
| Feature branch rot | Long-lived feature branches become impossible to merge back to develop | Rebase features on develop at least every 2-3 days; set a 2-week max lifetime |
| Tag collision | Two releases get the same version tag | Enforce semantic versioning in CI; reject duplicate tags in pre-receive hooks |
| Develop is broken | Someone merges broken code to develop, blocking all features | Require CI to pass before merge; use protected branches |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Structure | Clear roles for every branch type | Overhead for small teams |
| Release management | Dedicated release branch for stabilization | Slows down time-to-production |
| Hotfixes | Fast path from production fix to release | Merge conflicts between hotfix and develop |
| History | Clean, readable topology with —no-ff merges | More merge commits than linear history |
| Parallel work | Multiple features and releases simultaneously | Context switching between branches |
| Learning curve | Well-documented, widely understood | New developers must learn five branch types |
Implementation Snippets
Git Flow CLI Cheat Sheet
# Initialize
git flow init
# Features
git flow feature start <name>
git flow feature finish <name>
git flow feature publish <name>
git flow feature pull <name>
# Releases
git flow release start <version>
git flow release finish <version>
# Hotfixes
git flow hotfix start <version>
git flow hotfix finish <version>
# Support (long-term support branches)
git flow support start <version>
git flow support finish <version>
CI Integration — Protecting Branches
# .github/workflows/git-flow.yml
name: Git Flow Validation
on:
pull_request:
branches: [main, develop, "release/**"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify --no-ff merge
run: |
git log --oneline --graph -20
# Verify merge commits exist for feature branches
- name: Version tag check
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
TAG=${GITHUB_REF#refs/tags/}
if ! echo "$TAG" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Tag must follow semver: $TAG"
exit 1
fi
fi
Pre-commit Hook — Prevent Direct Commits to main
#!/bin/bash
# .git/hooks/pre-commit
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
if [ "$BRANCH" = "main" ]; then
echo "ERROR: Direct commits to main are not allowed."
echo "Use hotfix branches for production fixes."
exit 1
fi
if [ "$BRANCH" = "develop" ]; then
echo "WARNING: Direct commits to develop are discouraged."
echo "Use feature branches instead."
fi
Observability Checklist
- Logs: Track branch creation and merge events in your CI/CD pipeline logs
- Metrics: Measure average feature branch lifetime, release branch duration, and hotfix frequency
- Traces: Correlate release tags with deployment events and incident reports
- Alerts: Alert when release branches exceed 2 weeks, when
develophas failing CI, or when hotfix rate exceeds 2 per month - Dashboards: Track branches-per-developer, merge conflict rate, and time-from-feature-start-to-production
Security and Compliance Notes
- Branch protection: Enable required reviews and status checks on
mainanddevelopin GitHub/GitLab settings - Signed tags: Use
git tag -sfor release tags to cryptographically sign version markers - Access control: Restrict who can merge to
main(release managers only) anddevelop(senior developers) - Audit trail: The
--no-ffmerge strategy provides a clear audit trail of which features shipped in which release - Compliance: For regulated industries, release branches serve as change management artifacts — document what was tested and approved
Common Pitfalls and Anti-Patterns
- The Forever Release — A release branch that lives for months becomes a second
develop. Time-box releases strictly. - Skipping develop — Committing directly to
mainfor “small changes” breaks the model. Use hotfix branches for everything. - Feature branch hoarding — Developers who keep features local for weeks create merge nightmares. Push and open PRs early.
- Release branch feature creep — Adding new features to a release branch defeats its purpose. Only bug fixes belong there.
- Ignoring develop sync — Not merging
developinto active release branches causes painful integration at the end. - Tag-only releases — Tagging
developinstead ofmainmeans your production tag points to untested code. - No rebase policy — Feature branches that never rebase on
developaccumulate divergence. Rebase regularly.
Quick Recap Checklist
-
mainbranch contains only production-ready, tagged code -
developbranch is the integration point for all features - Features branch from and merge back to
developwith--no-ff - Release branches are created from
develop, merged to bothmainanddevelop - Hotfixes branch from
main, merge to bothmainanddevelop - All releases are tagged with semantic versions
- Branch protection rules prevent direct commits to
main - CI validates all merges to
developandrelease/* - Release branches are time-boxed to 1-2 weeks
- Feature branches are rebased on
developregularly
Branching Strategy Comparison
| Aspect | Git Flow | GitHub Flow | Trunk-Based Development |
|---|---|---|---|
| Best team size | 10-100 | 1-20 | 20-10000+ |
| Release cadence | Scheduled (weeks) | Continuous (minutes) | Continuous (multiple/day) |
| Branch count | 5 types | 2 types | 1 permanent + short-lived |
| Complexity | High | Low | Medium |
| CI requirement | Moderate | Strong | Very strong |
| Feature flags | Optional | Optional | Required |
| Hotfix path | Direct from main | Same as any PR | Flag toggle or quick PR |
| Learning curve | Steep (5 branch types) | Gentle (1 rule) | Moderate (discipline-heavy) |
| Merge frequency | Low (per release) | Medium (per feature) | Very high (multiple/day) |
| Multi-version | Yes (support branches) | No | No |
Interview Q&A
A release branch is created from develop to prepare for an upcoming production release. It allows final bug fixes, version bumps, and documentation updates without blocking new feature development on develop. It merges to both main and develop.
A hotfix branch is created from main to address a critical production issue that cannot wait for the next scheduled release. It bypasses develop entirely for speed, then merges back to both main and develop to prevent the bug from reappearing.
--no-ff merges for feature branches?The --no-ff (no fast-forward) flag forces Git to create a merge commit even when a fast-forward merge would be possible. This preserves the topology of the feature branch in the commit history, making it clear which commits belonged to which feature.
Without --no-ff, the feature branch's commits would be linearly applied to develop, losing the visual grouping that shows what was developed together. This is especially valuable for auditing and understanding release contents.
The bug fix exists in main and is deployed to production, but develop still contains the buggy code. When the next release is created from develop, the bug reappears because the hotfix was never integrated into the development stream.
This is one of the most common Git Flow mistakes. The git flow hotfix finish command handles both merges automatically, which is why using the CLI extension is recommended over manual branch management.
Technically yes, but it is not a good fit. Git Flow assumes a separation between "integration" (develop) and "release" (release branch), which contradicts the continuous deployment principle of deploying every passing merge to main.
Teams practicing continuous deployment typically use GitHub Flow or Trunk-Based Development instead, where every merge to main is deployable and feature flags control rollout rather than branch management.
Git Flow supports this through support branches. When you need to maintain v1.x while developing v2.x, you create a support branch from the last v1.x release tag:
git flow support start 1.x
Hotfixes for v1.x branch from the support branch, not from main. This keeps the maintenance stream isolated from active development. However, managing multiple support branches adds significant complexity, which is why many teams prefer to minimize the number of actively maintained versions.
Resources
- A successful Git branching model — Vincent Driessen’s original 2010 post
- git-flow CLI — The official git-flow command-line extension
- Atlassian Git Flow tutorial — Detailed walkthrough with diagrams
- Git Flow considered harmful — Critical analysis of Git Flow’s limitations
- Semantic Versioning 2.0.0 — Version numbering standard used with Git Flow tags
Category
Related Posts
Choosing a Git Team Workflow: Decision Framework for Branching Strategies
Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.
GitLab Flow: Environment and Release-Based Branching
Master GitLab Flow — the branching strategy that combines Git Flow simplicity with deployment pipelines. Learn environment-based and release-based branching patterns.
Automated Release Pipeline: From Git Commit to Production Deployment
Build a complete automated release pipeline with Git, CI/CD, semantic versioning, changelog generation, and zero-touch deployment. Hands-on tutorial for production-ready releases.