Home DevOps Git GitFlow Workflow Explained — Branches, Releases and Real-World Usage

Git GitFlow Workflow Explained — Branches, Releases and Real-World Usage

In Plain English 🔥
Imagine a busy restaurant kitchen. There's a prep area where chefs experiment with new dishes, a staging area where meals are plated and checked before leaving the kitchen, and the pass where only perfect plates go out to customers. GitFlow is exactly that system — but for your codebase. Every new feature gets its own prep space, nothing reaches production until it's been checked in staging, and if something goes wrong with a dish already at the table, there's a dedicated rescue process that doesn't halt the whole kitchen.
⚡ Quick Answer
Imagine a busy restaurant kitchen. There's a prep area where chefs experiment with new dishes, a staging area where meals are plated and checked before leaving the kitchen, and the pass where only perfect plates go out to customers. GitFlow is exactly that system — but for your codebase. Every new feature gets its own prep space, nothing reaches production until it's been checked in staging, and if something goes wrong with a dish already at the table, there's a dedicated rescue process that doesn't halt the whole kitchen.

Most teams that struggle with deployments aren't bad at writing code — they're bad at managing change. Features half-done get pushed to production. A hotfix for a critical bug accidentally ships an unfinished feature alongside it. Two developers overwrite each other's work on the same branch. These aren't skill problems; they're workflow problems. And in a world where a single bad deploy can cost thousands in downtime or lost revenue, having a repeatable, structured branching strategy isn't optional — it's essential.

GitFlow, introduced by Vincent Driessen in 2010, solves exactly this chaos by giving every type of change its own lane on the road. New features live in isolation until they're ready. Releases get a dedicated stabilisation window before they go live. Emergency fixes can be applied to production without dragging half-finished work along for the ride. The model is opinionated — and that's the point. Opinions eliminate ambiguity, and ambiguity is what causes 2am incidents.

By the end of this article you'll understand not just the five branch types in GitFlow and how they relate to each other, but — more importantly — when GitFlow is the right tool and when it'll slow you down. You'll be able to set up a full GitFlow project from scratch, navigate a real-world feature-to-release cycle, apply an emergency hotfix without breaking anything, and walk into an engineering interview and confidently explain the trade-offs of GitFlow versus trunk-based development.

The Five-Branch Architecture — Why Each Branch Exists

GitFlow uses five branch types, and understanding the purpose of each is more important than memorising their names. Two branches are permanent and never get deleted: main and develop. Three are temporary and get cleaned up after use: feature/, release/, and hotfix/*.

main represents what's in production right now. Every commit on main is a tagged, deployed release. Nothing ever gets committed directly to main — it only ever receives merges from release or hotfix branches. Think of it as the restaurant's dining room: only finished, approved dishes arrive here.

develop is the integration branch — the staging kitchen. It holds the latest completed work that's been approved for the next release. Feature branches are cut from develop and merge back into develop when done. It's always ahead of main, and it might be slightly unstable on any given day, but it's never a mess.

Temporary branches are where the actual work happens. A feature/user-authentication branch gives one team or developer complete isolation. A release/2.4.0 branch freezes the feature set and allows only bug fixes before shipping. A hotfix/fix-payment-gateway-null-crash branch lets you patch production without touching any in-progress work. Every branch type has a strict source and a strict destination — that structure is what prevents the chaos described in the introduction.

gitflow-project-setup.sh · BASH
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# Full GitFlow project setup from scratch — no plugins required.
# This uses plain Git commands so you understand what's happening
# under the hood before you reach for the git-flow CLI extension.
# ─────────────────────────────────────────────────────────────

# Step 1: Create a new repo and establish the permanent branches.
git init ecommerce-platform
cd ecommerce-platform

# Create an initial commit so 'main' has a history to branch from.
echo "# E-Commerce Platform" > README.md
git add README.md
git commit -m "chore: initial project setup"

# Rename the default branch to 'main' if your Git version defaults to 'master'.
git branch -M main

# Step 2: Create the 'develop' branch from 'main'.
# 'develop' is the long-lived integration branch — all feature work lands here.
git checkout -b develop

# At this point we have our two permanent branches.
git branch
# * develop
#   main

# Step 3: Start a new feature branch.
# Feature branches are ALWAYS cut from 'develop', never from 'main'.
# The 'feature/' prefix is a convention, not a Git requirement — but follow it.
git checkout -b feature/user-authentication develop

# Simulate some development work on the feature.
echo "def authenticate_user(email, password): pass" > auth.py
git add auth.py
git commit -m "feat(auth): add user authentication scaffold"

echo "def validate_token(token): pass" >> auth.py
git add auth.py
git commit -m "feat(auth): add JWT token validation"

# Step 4: Merge the completed feature back into 'develop'.
# --no-ff (no fast-forward) forces a merge commit, which preserves the
# history of the feature branch in the graph. Without this, the feature's
# commits get absorbed into develop's linear history and you lose visibility.
git checkout develop
git merge --no-ff feature/user-authentication -m "merge: integrate user authentication feature into develop"

# Clean up the local feature branch — it's served its purpose.
git branch -d feature/user-authentication

# Step 5: Cut a release branch when develop is ready to ship.
# Release branches are cut from 'develop'. Only bug fixes, docs updates,
# and release-preparation commits should land here. No new features.
git checkout -b release/1.0.0 develop

# Simulate a pre-release bug fix found during QA.
echo "# auth module v1.0.0" >> auth.py
git add auth.py
git commit -m "fix(auth): handle empty password edge case before release"

# Step 6: Finalise the release — merge into BOTH main AND develop.
# main gets the release so production is updated.
git checkout main
git merge --no-ff release/1.0.0 -m "release: ship v1.0.0 to production"
git tag -a v1.0.0 -m "Version 1.0.0 — initial public release"

# develop must also receive the release branch changes (e.g., the QA bug fix)
# so that the fix isn't lost in future work.
git checkout develop
git merge --no-ff release/1.0.0 -m "merge: back-port release/1.0.0 fixes into develop"

# Clean up the release branch.
git branch -d release/1.0.0

echo "GitFlow project structure established successfully."
▶ Output
Initialized empty Git repository in /projects/ecommerce-platform/.git/
[main (root-commit) 4a1b2c3] chore: initial project setup
1 file changed, 1 insertion(+)
Switched to a new branch 'develop'
* develop
main
Switched to a new branch 'feature/user-authentication'
[feature/user-authentication 7d3e4f5] feat(auth): add user authentication scaffold
[feature/user-authentication 9a1b0c2] feat(auth): add JWT token validation
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch feature/user-authentication (was 9a1b0c2).
Switched to a new branch 'release/1.0.0'
[release/1.0.0 2f8d1e9] fix(auth): handle empty password edge case before release
Switched to branch 'main'
Merge made by the 'ort' strategy.
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch release/1.0.0 (was 2f8d1e9).
GitFlow project structure established successfully.
⚠️
Watch Out: Always Use --no-ff When Merging Feature BranchesIf you merge a feature branch into develop with a fast-forward merge (the default), Git will linearise the commits and you'll lose all visual evidence that those commits belonged to a feature. Six months later, your `git log --graph` will be a straight line and a `git bisect` or blame session becomes needlessly painful. Always pass `--no-ff` to preserve the merge commit and the branch context in your history.

Handling Emergency Production Bugs With Hotfix Branches

Here's the scenario no one wants but every team eventually faces: it's Thursday afternoon, version 1.0.0 is live, and a critical payment processing bug is crashing checkouts for 15% of users. Meanwhile, develop already has three half-finished features merged into it that absolutely cannot ship with this emergency fix.

This is exactly why hotfix branches exist — and why their source branch being main (not develop) is so deliberate. By branching from main, you get a clean copy of exactly what's in production, untainted by anything on develop. You fix only the bug, merge back to main, tag a new patch version, and then — critically — also merge back to develop so the fix isn't lost.

That last step trips people up constantly. If you only merge the hotfix into main and forget develop, the bug you just fixed will silently re-appear in your next scheduled release when develop gets merged to main. It's one of the most insidious mistakes in GitFlow practice.

The hotfix branch also signals urgency to your team through its name. When someone sees hotfix/fix-payment-null-pointer, everyone on the team immediately knows this isn't routine work — it's a production incident, and it has priority.

gitflow-hotfix-cycle.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# GitFlow Hotfix CycleEmergency production bug repair.
# Scenario: v1.0.0 is live. A null pointer crash is breaking
# the payment gateway for a subset of users. develop has
# unfinished work that cannot ship. We must fix prod NOW.
# ─────────────────────────────────────────────────────────────

# Step 1: Branch from 'main'NOT from 'develop'.
# main = what's in production. develop = what's coming next.
# We want a clean snapshot of prod to work from.
git checkout main
git checkout -b hotfix/fix-payment-gateway-null-crash

# Step 2: Apply the targeted fix.
# In a real scenario this is where your developer fixes the bug.
cat > payment_gateway.py << 'EOF'
def process_payment(order_id, payment_details):
    # HOTFIX v1.0.1: Added None guard — payment_details was not
    # validated upstream, causing a NullPointerError on orders
    # created via the mobile API which omits the billing_zip field.
    if payment_details is None:
        raise ValueError("payment_details cannot be None — check mobile API payload")
    
    billing_zip = payment_details.get("billing_zip", "00000")  # safe default
    return {"status": "approved", "order_id": order_id, "zip": billing_zip}
EOF

git add payment_gateway.py
git commit -m "fix(payments): guard against None payment_details from mobile API"

# Step 3: Update the version number in the project metadata.
# This is important — production tags must be unique and meaningful.
echo "1.0.1" > VERSION
git add VERSION
git commit -m "chore(release): bump version to 1.0.1 for hotfix"

# Step 4: Merge hotfix into 'main' and tag the new production version.
git checkout main
git merge --no-ff hotfix/fix-payment-gateway-null-crash \
  -m "hotfix: merge payment gateway null crash fix into main"
git tag -a v1.0.1 -m "Version 1.0.1 — Emergency fix for payment gateway null crash"

# Step 5: CRITICAL — merge hotfix into 'develop' too.
# If you skip this, the bug will resurface in v1.1.0 when develop
# gets merged to main. This step is the one most teams forget.
git checkout develop
git merge --no-ff hotfix/fix-payment-gateway-null-crash \
  -m "hotfix: back-port payment gateway null crash fix into develop"

# Step 6: Delete the hotfix branch — it has served its purpose.
git branch -d hotfix/fix-payment-gateway-null-crash

# Verify the tag exists on main
git checkout main
git log --oneline --graph -5

echo "Hotfix v1.0.1 shipped and back-ported to develop successfully."
▶ Output
Switched to branch 'main'
Switched to a new branch 'hotfix/fix-payment-gateway-null-crash'
[hotfix/fix-payment-gateway-null-crash 3c7f2a1] fix(payments): guard against None payment_details from mobile API
[hotfix/fix-payment-gateway-null-crash 8b4e9d0] chore(release): bump version to 1.0.1 for hotfix
Switched to branch 'main'
Merge made by the 'ort' strategy.
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch hotfix/fix-payment-gateway-null-crash (was 8b4e9d0).
Switched to branch 'main'
* 5e1a3b2 (HEAD -> main, tag: v1.0.1) hotfix: merge payment gateway null crash fix into main
|\
| * 8b4e9d0 chore(release): bump version to 1.0.1 for hotfix
| * 3c7f2a1 fix(payments): guard against None payment_details from mobile API
|/
* 4d9c8f1 (tag: v1.0.0) release: ship v1.0.0 to production
Hotfix v1.0.1 shipped and back-ported to develop successfully.
⚠️
Pro Tip: Use Semantic Versioning Tags ConsistentlyGitFlow and semantic versioning (MAJOR.MINOR.PATCH) are natural partners. Hotfixes bump PATCH (1.0.0 → 1.0.1). Feature releases bump MINOR (1.0.0 → 1.1.0). Breaking changes bump MAJOR (1.0.0 → 2.0.0). Enforcing this in your team means anyone reading `git tag` instantly knows the risk level of each release without reading a changelog.

GitFlow vs Trunk-Based Development — Choosing the Right Workflow

GitFlow is a powerful model, but it isn't the right model for every team or every product — and knowing when not to use it is as important as knowing how to use it.

GitFlow shines when your team ships scheduled, versioned releases — think desktop software, mobile apps, open-source libraries, or enterprise SaaS with quarterly release windows. The structured branch lifecycle gives you clear separation between what's done, what's being stabilised, and what's in progress. QA teams love it because there's an explicit release branch to test against. Ops teams love it because main is always a known-good, tagged state.

Trunk-based development, by contrast, is better for teams deploying multiple times a day. Everyone commits to a single main branch (or very short-lived feature branches). Feature flags control what's visible in production. The overhead of maintaining five branch types would actively slow these teams down.

The honest answer: if your CI/CD pipeline deploys to production on every merge and your team has mature feature flagging, trunk-based is likely faster for you. If you have a QA cycle, multiple environments, compliance requirements, or a public API with versioned releases, GitFlow's structure pays for itself many times over.

gitflow-cli-quickstart.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# Using the git-flow CLI extension — the faster way once you
# understand the underlying model. Install first:
#   macOS:  brew install git-flow-avh
#   Ubuntu: apt-get install git-flow
#   Windows: included in Git for Windows
# ─────────────────────────────────────────────────────────────

# Initialise GitFlow in an existing repo.
# This sets up the branch naming conventions in .git/config.
# Accept all the defaults by pressing Enter through the prompts,
# or use -d flag for fully non-interactive default setup.
git flow init -d

# ── FEATURE WORKFLOW ──────────────────────────────────────────
# Start a feature — automatically branches from 'develop'
git flow feature start shopping-cart-persistence

# ... do your work, make commits ...
echo "def save_cart(user_id, cart_items): pass" > cart.py
git add cart.py
git commit -m "feat(cart): implement session-based cart persistence"

# Finish the feature — merges into develop with --no-ff, deletes branch
git flow feature finish shopping-cart-persistence
# Output: Switched to branch 'develop'
#         Merge made by the 'ort' strategy.
#         Deleted branch feature/shopping-cart-persistence

# ── RELEASE WORKFLOW ──────────────────────────────────────────
# Start a release branch from develop's current state
git flow release start 1.1.0

# Apply only bug fixes and release prep commits here
echo "1.1.0" > VERSION
git add VERSION
git commit -m "chore(release): bump version to 1.1.0"

# Finish the release:
# - merges into main AND develop
# - creates a tag automatically
# - deletes the release branch
# -m sets the tag message without opening an editor
git flow release finish -m "Version 1.1.0 — Shopping cart persistence" 1.1.0

# ── HOTFIX WORKFLOW ───────────────────────────────────────────
# Start a hotfix from main (production)
git flow hotfix start fix-cart-total-rounding-error

# Fix the bug
echo "def calculate_total(items): return round(sum(i.price for i in items), 2)" >> cart.py
git add cart.py
git commit -m "fix(cart): correct floating-point rounding in total calculation"

# Finish hotfix — merges to main AND develop, tags, deletes branch
git flow hotfix finish -m "Hotfix: cart total rounding" fix-cart-total-rounding-error

# Verify your tag history
git tag --list
echo "All workflows complete."
▶ Output
Using default branch names.

Summary of actions:
- A new branch 'develop' was created
- You are now on branch 'develop'

Switched to a new branch 'feature/shopping-cart-persistence'
[feature/shopping-cart-persistence 1a2b3c4] feat(cart): implement session-based cart persistence
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch feature/shopping-cart-persistence (was 1a2b3c4).

Switched to a new branch 'release/1.1.0'
[release/1.1.0 5d6e7f8] chore(release): bump version to 1.1.0
Switched to branch 'main'
Merge made by the 'ort' strategy.
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch release/1.1.0 (was 5d6e7f8).

Switched to a new branch 'hotfix/fix-cart-total-rounding-error'
[hotfix/fix-cart-total-rounding-error 9g0h1i2] fix(cart): correct floating-point rounding in total calculation
Switched to branch 'main'
Merge made by the 'ort' strategy.
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch hotfix/fix-cart-total-rounding-error (was 9g0h1i2).

v1.0.0
v1.0.1
v1.1.0
All workflows complete.
🔥
Interview Gold: Know When to Argue Against GitFlowInterviewers at mature companies don't just want to know if you can follow GitFlow — they want to know if you understand its trade-offs. Be ready to say: 'GitFlow adds overhead that pays off for versioned releases but can slow down teams doing continuous deployment. I'd recommend trunk-based development with feature flags for a team shipping 10+ times a day.' That kind of nuance separates senior candidates from those who just memorised the diagram.
AspectGitFlowTrunk-Based Development
Permanent branchesmain + developmain only
Feature isolationDedicated feature/* branchesShort-lived branches or direct commits
Release managementExplicit release/* branch with stabilisation windowFeature flags control release visibility
Hotfix processhotfix/* from main, merged to main + developCommit directly to main, deploy immediately
Best suited forScheduled releases, versioned APIs, mobile appsSaaS with continuous deployment, high-frequency releases
Branch complexityHigh — 5 branch types with strict rulesLow — one branch, maximum simplicity
Parallel version supportExcellent — maintain v1.x and v2.x simultaneouslyDifficult — requires additional branching strategy
CI/CD compatibilityWorks well with staged environments (dev/staging/prod)Optimal — every commit can be production-ready
Team size sweet spotMedium to large teams with defined rolesAny size, especially high-trust senior teams
Risk of merge conflictsHigher — long-lived feature branches diverge moreLower — frequent integration keeps branches short

🎯 Key Takeaways

  • GitFlow's two permanent branches (main and develop) serve completely different purposes — main is always production-ready and tagged; develop is the integration point for completed features and is allowed to be slightly unstable day-to-day.
  • Hotfix branches must always branch from main and merge into BOTH main AND develop — skipping the develop merge is the single most common GitFlow mistake and causes previously fixed bugs to reappear in future releases.
  • The --no-ff flag on every GitFlow merge is not optional — it preserves the visual history of branch boundaries in your Git graph, which is critical for debugging regressions and understanding what changed in each release.
  • GitFlow is the right choice for scheduled, versioned releases; it's the wrong choice for teams deploying multiple times per day — matching your branching strategy to your deployment cadence is more important than following any single model dogmatically.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting to merge the hotfix branch back into develop — Symptom: The bug you just fixed in v1.0.1 silently reappears in v1.1.0. You'll only catch it during QA if you're lucky, or in production if you're not — Fix: Make it a team rule and a CI check. After every git flow hotfix finish, immediately run git log develop --oneline | head -5 and verify the hotfix commit SHA appears in develop's history. Some teams add a post-merge hook that reminds developers of this step.
  • Mistake 2: Committing new features onto a release branch — Symptom: Your release branch grows larger than planned, QA scope creeps, and the release gets delayed — Fix: The release branch is a stabilisation zone, not a feature addition zone. Establish a written team policy: the only commits allowed on a release branch are bug fixes, documentation updates, and version bumps. If a developer wants to sneak in a small feature, it waits for the next cycle. Enforce this with pull request guidelines and code review.
  • Mistake 3: Using fast-forward merges when finishing feature branches — Symptom: git log --graph on develop shows a straight line of commits with no visible trace of which feature each commit belonged to. Bisecting a regression becomes a nightmare because you can't isolate feature boundaries — Fix: Always use --no-ff (no fast-forward) on all GitFlow merges. If you're using the git-flow CLI extension, this is handled automatically. If you're using raw Git, add a Git alias: git config --global alias.mff 'merge --no-ff' so the habit is built into your muscle memory.

Interview Questions on This Topic

  • QWalk me through exactly what happens — step by step — when a critical bug is discovered in production on a Friday afternoon and your team is using GitFlow. Which branches are involved, in what order, and why?
  • QWhy does GitFlow require merging a release branch into both main AND develop? What specific problem does that double-merge solve, and what breaks if you only merge into main?
  • QA colleague suggests your team should switch from GitFlow to trunk-based development. How do you evaluate whether that's the right move, and what questions would you ask about your team's current process before making a recommendation?

Frequently Asked Questions

What is the difference between GitFlow and GitHub Flow?

GitFlow uses five branch types (main, develop, feature, release, hotfix) and is designed for scheduled, versioned releases with a stabilisation window. GitHub Flow is much simpler — it uses just one main branch and short-lived feature branches that merge directly to main after a pull request review. GitHub Flow suits teams deploying continuously; GitFlow suits teams with defined release cycles and QA gates between development and production.

Do I need to use the git-flow CLI extension to use GitFlow?

No — GitFlow is a branching model, not a tool. The git-flow CLI extension automates the branch creation, merging, and deletion steps, but everything it does is plain Git under the hood. Learning the raw Git commands first (as shown in this article) is genuinely worth the time because it means you understand what's happening and can troubleshoot when something goes wrong, rather than being dependent on the tool.

Can you have multiple feature branches active at the same time in GitFlow?

Yes, and this is one of GitFlow's core strengths. Each developer or team works on their own isolated feature/* branch simultaneously. They all eventually merge back into develop independently. The key discipline is keeping feature branches short-lived — the longer a feature branch lives, the more it diverges from develop and the harder the eventual merge becomes. If a feature will take more than a week or two, consider using feature flags to merge incomplete work safely.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousGit Stash and Cherry-pickNext →GitHub Pull Requests and Code Review
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged