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 started it all. Vincent Driessen published his branching model in 2010, and it became the default for teams shipping on a calendar for over a decade. The model separates concerns across five branch types, each with a clear role and lifecycle. If you have ever worked on a team with scheduled releases, long support windows, or separate dev and QA cycles, you have probably encountered 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.
Introduction
Vincent Driessen introduced Git Flow in 2010 to bring order to teams working in parallel. His model centers on five branch types — main, develop, feature, release, and hotfix — each with its own entry and exit rules. It assumes you ship on a schedule and need a clear separation between integration, stabilization, and production. Even if you have moved on to simpler workflows, Git Flow shaped how teams think about collaboration, and every modern strategy exists as a reaction to it.
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
| 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-off Analysis
| 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 / 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 Questions
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.
develop branch in Git Flow, and why is it a permanent branch?The develop branch serves as the integration branch where all completed features are merged before a release. It is permanent because:
- Feature branches always branch from and merge back to
develop, making it the single source of truth for completed work - Release branches are created from
develop, so it must always reflect the latest integrated features - Hotfixes merge to both
mainanddevelop, keepingdevelopin sync with production fixes - If
developis broken, no new features can be merged, blocking the entire team's progress
The feature branch lifecycle follows these steps:
- Create:
git flow feature start feature-name— branches from the currentdevelopHEAD - Develop: Multiple commits on the feature branch; regular rebasing on
developrecommended - Publish:
git flow feature publish feature-name— pushes to remote for CI feedback and code review - Review: Pull request with required checks, at least one approval from a peer
- Finish:
git flow feature finish feature-name— merges todevelopwith--no-ff, deletes the local and remote branch
The --no-ff merge creates a merge commit that groups all feature commits together in the history.
main and develop?A release branch merges to both branches for different reasons:
- To
main: Applies the release tag and marks the official version in production history - To
develop: Syncs any last-minute bug fixes made during release stabilization back into the development stream so those fixes are not lost when the next release branch is created
This bidirectional merge ensures the release branch is essentially a synchronization point between production and development.
develop during a long release?When a release branch lives too long (weeks), develop continues advancing with new features. The longer the release branch exists, the larger the merge conflict when trying to sync back to develop. This creates a painful integration at the end of the release cycle.
Mitigation strategies:
- Time-box release branches to a maximum of 1-2 weeks
- Merge
developinto the release branch daily to catch conflicts early - Freeze new feature merges to
developduring the final days of a release
Enterprise Git Flow requires layered branch protection:
main: Require 2 approvals, require signed commits, require CI to pass, admin cannot bypassdevelop: Require 1 approval, require CI to pass, linear history enforcedrelease/*: Require CI to pass, require at least one release manager approvalfeature/*: Require CI to pass, require at least one peer approval before merge todevelophotfix/*: Require expedited review (2 hours SLA), CI must pass, emergency approval by on-call lead
Git Flow uses annotated tags on main to mark releases following semantic versioning (major.minor.patch):
git tag -a v1.0.0 -m "Release 1.0.0"— creates an annotated tag when finishing a release- Hotfixes increment the patch version:
v1.0.1 - Releases increment minor or major versions:
v1.1.0orv2.0.0 - Tags are signed with
-sfor cryptographic authenticity in enterprise environments
Key differences:
- Branch lifetime: Git Flow uses long-lived permanent branches (
main,develop) with temporary feature branches; Trunk-Based Development uses only short-lived feature branches (max 1-2 days) branching from a singlemain - Integration point: Git Flow integrates on
develop; Trunk-Based Development integrates directly onmain - Release model: Git Flow uses dedicated release branches; Trunk-Based Development uses feature flags to control rollout
- Team size fit: Git Flow scales well for 10-100 people; Trunk-Based Development works for 20-10000+ with strong CI/CD
- Learning curve: Git Flow has a steep learning curve with 5 branch types; Trunk-Based Development has moderate complexity but requires strong discipline
git flow feature finish and merges directly to develop, what problems arise?Direct merges to develop (bypassing the CLI workflow) create several problems:
- Missing branch deletion: The feature branch is not automatically deleted, causing branch hoarding
- No
--no-ffenforcement: Manual merges may use fast-forward, losing the feature branch topology in history - Inconsistent history: Other developers following the CLI workflow have merge commits, while direct-merging developers do not
- Broken git flow tooling: The
git flow featurecommands track branch finish state; manual merges break this tracking
develop is broken by a bad merge?Recovery steps when develop is broken:
- Identify the bad merge:
git log --oneline develop ^origin/developto find commits not on remote - Revert the merge:
git revert -m 1 <merge-commit-hash>for a clean reversal that does not rewrite history - Verify CI passes: The revert commit triggers CI; if it passes,
developis healthy again - Notify the team: Post in the team channel that the broken merge was reverted so developers know to re-merge their fixes
- Fix forward: The original author can re-implement the fix correctly and reopen the PR after CI passes
The git-flow CLI provides automation but introduces trade-offs:
- Convenience vs lock-in: CLI automates branch creation/finishing but teams become dependent on the tool for correct workflow execution
- Defaults vs flexibility: The CLI enforces convention (branch prefixes, merge strategies) but can be overridden manually when needed
- Learning curve vs error reduction: Teams must learn the CLI syntax, but it prevents common mistakes like forgetting to merge hotfixes back to
develop - Automation vs transparency: CLI hides the underlying Git commands, which can make troubleshooting harder when something goes wrong
Support branches enable long-term maintenance versions of a product. When you need to maintain multiple major versions simultaneously (e.g., v1.x in security patch mode while v2.x is in active development), you create a support branch from the last release tag of that version.
Key characteristics:
- Created from release tags, not from
mainordevelop - Hotfixes branch from the support branch and merge back to it and
main - Does not merge forward to newer versions — each support branch is independently maintained
- Best for products with scheduled release cycles and customers on specific version tiers
Git Flow has significantly higher release management overhead than GitHub Flow:
- Release branches: Git Flow requires creating, stabilizing, and finishing release branches. GitHub Flow has no release branches — every merge to
mainis immediately deployable. - Version tags: Git Flow requires manual version bump commits and tag management. GitHub Flow typically auto-generates version numbers from CI.
- Hotfix path: Git Flow has a dedicated hotfix branch type. GitHub Flow uses regular feature branches with expedited review.
- Scheduled vs continuous: Git Flow assumes releases are scheduled events. GitHub Flow assumes continuous deployment.
Git Flow's overhead is justified for enterprises with formal release processes; GitHub Flow's simplicity is better for fast-moving teams.
--no-ff in Git Flow?Fast-forward merges in Git Flow lose critical information:
- Feature topology: Commits from a 2-week feature branch get interleaved with other commits on
develop, making it impossible to see which commits belong together - Release contents: When auditing a release, you cannot determine which features shipped without analyzing commit messages manually
- Revert complexity: Reverting a feature requires reverting individual commits, not a single merge commit
- Bisect difficulty:
git bisectbecomes less useful when feature commits are scattered across history
The --no-ff flag ensures every feature merge creates a clear grouping commit that preserves the branch topology for future reference.
develop?When a feature branch has diverged significantly:
- Rebase first:
git flow feature rebasereplays the feature commits on top of the currentdevelopHEAD, creating a cleaner topology - Merge conflicts: During rebase, you resolve conflicts one commit at a time rather than all at once
- Force push: After rebasing, you must force-push the feature branch since the commit history has changed
- Alternative: merge: If rebase is too risky,
git merge developinto the feature branch creates a merge commit but preserves existing commits
Regular rebasing (every 2-3 days) prevents significant divergence and makes the finish operation smoother.
Key Git Flow metrics for team health:
- Feature branch age: Average days from creation to merge. Target: under 7 days. Long branches indicate scope creep or review bottlenecks.
- Release branch duration: How long release branches live. Target: 1-2 weeks max. Longer indicates stabilization problems.
- Hotfix frequency: Number of production fixes per month. High frequency indicates quality issues in the development process.
- Develop CI green time: Percentage of time
developpasses all checks. Low percentage blocks all features. - Merge conflict rate: Percentage of feature merges requiring conflict resolution. High rate indicates branches are too old.
- Rebase vs merge ratio: How often teams rebase vs merge features. More rebases indicate active branch hygiene.
Further Reading
- 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
Conclusion
Git Flow brought structure to branching with its main/develop/release/hotfix hierarchy. It earns its place on projects with scheduled releases and separate QA cycles. The overhead is real though — if you are deploying on every merge, look at simpler strategies. Git Flow shines when you need strict release management, not when you are shipping continuously.
Category
Related Posts
Choosing a Git Team Workflow: Decision Framework
Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.
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.