Improve Your Git Workflow with Git Flow
Git Flow is a structured branching model built around versioned, scheduled releases. Here is how its branches fit together, a hands-on walkthrough of features, releases and hotfixes, and an honest take on when it is still the right call.
Git Flow is a development workflow for keeping branching consistent across a team. It was the workflow everyone copied for years, and it is still a great fit for certain kinds of projects. In this article I will walk through the branches it uses, show a hands-on example of features, releases and hotfixes, and give you an honest read on when to reach for it.
Is Git Flow still the right choice?
Let's get the reality check out of the way first, because it changes how you should read the rest of this post.
Git Flow comes from Vincent Driessen's 2010 article "A successful Git branching model." In 2020 he added a note to the top of that same article saying that if your team practices continuous delivery, you should probably adopt something simpler like GitHub Flow, and that Git Flow still fits teams building explicitly versioned software or supporting multiple versions in the wild. Atlassian now describes Git Flow as a legacy workflow, one that has lost ground to trunk-based development for modern CI/CD.
None of that makes it wrong. It just means the extra branches are overhead you only want to pay for when versioned releases actually earn it. Think desktop apps, libraries, mobile releases, SDKs, anything where multiple versions live in the world at once and need separate maintenance. If you are shipping a web app that deploys continuously, skip ahead to GitHub Flow, or read my Git Flow vs GitHub Flow comparison for the full head to head.
Still here? Then you probably ship versions, and Git Flow's structure is going to serve you well. Let's get into it.
The branches
Git Flow uses two long-lived branches plus three kinds of supporting branches.
mainis your currently released code, what you would find in production. (In the original 2010 model this branch was calledmaster; modern repos default tomain, and that is what I will use throughout.)developis the integration branch where finished work collects between releases.
On top of those two, you create short-lived supporting branches as you need them.
feature/*branches come offdevelopfor each new piece of work.release/*branches freezedevelopso you can harden a version before shipping it.hotfix/*branches come offmainto patch production in a hurry.
The golden rule: you never commit directly to main or develop. Everything arrives through a supporting branch.
Setting up the project
The best way to learn this is by doing, so let's set up a fresh repository. Inside your project directory, create the first commit on main and push it up.
echo "# Git Flow Example" >> README.md
git init
git add README.md
git commit -m "Initial commit"
git remote add origin git@github.com:frankperez87/gitflow-example.git
git push -u origin main
That initial commit is the one exception to the never-touch-main rule; we are just bootstrapping the repo. From here on, main only changes through releases and hotfixes.
Next, create the develop branch and track it remotely. This is where features will land.
git branch develop
git push -u origin develop
Feature branches
Every new piece of work is a feature, unless it is an urgent production fix (that is a hotfix, which we will get to). Let's add a contributing guide. We branch off develop and prefix the branch with feature/.
git checkout -b feature/contributing develop
Now make the change. Keep it small and real.
echo "# Contributing" >> CONTRIBUTING.md
echo "Open a pull request against develop and keep changes focused." >> CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "Add contributing guide"
You can also name the branch
feature-contributingwith a dash instead of a slash. Both are fine; just pick one convention and stick to it.
With the feature done, merge it back into develop and delete the branch, since it has served its purpose.
git checkout develop
git merge feature/contributing
git branch -d feature/contributing
Release branches
When develop has enough finished work to ship, you cut a release branch. Release branches are prefixed with release/ followed by the version number. This freezes a snapshot of develop so you can do final hardening (version bumps, last-minute fixes, changelog) without blocking new feature work.
git checkout -b release/0.1.0 develop
echo "Version 0.1.0" >> CHANGELOG.md
git add CHANGELOG.md
git commit -m "Prepare 0.1.0 release"
When the release is ready, merge it into main, then tag that commit with the version.
git checkout main
git merge release/0.1.0
git tag -a v0.1.0 -m "Release 0.1.0"
git push origin main v0.1.0
Here is the step people forget: you also merge the release back into develop. Any fixes or version bumps you made on the release branch happened off develop's line, so merging them back keeps develop from falling behind production.
git checkout develop
git merge release/0.1.0
git push
git branch -d release/0.1.0
Hotfix branches
Now suppose a user finds a bug in production that cannot wait for the next release. A hotfix branches straight off main (not develop), because you want to patch exactly what is live.
git checkout -b hotfix/login-typo main
Make the smallest change that fixes the problem and commit it.
git commit -am "Fix typo on login button"
Just like a release, a hotfix merges into both main and develop so the fix is not lost, and the new version gets tagged on main. A bug fix is a backward-compatible change, so it bumps the PATCH number: 0.1.0 becomes 0.1.1.
git checkout main
git merge hotfix/login-typo
git tag -a v0.1.1 -m "Hotfix: login button typo"
git push origin main v0.1.1
git checkout develop
git merge hotfix/login-typo
git push
git branch -d hotfix/login-typo
A quick word on versioning
Those tags are not arbitrary. Git Flow pairs naturally with semantic versioning, where a version reads MAJOR.MINOR.PATCH:
- MAJOR for a breaking change.
- MINOR for new, backward-compatible functionality (your next feature release would be
v0.2.0). - PATCH for a backward-compatible bug fix (the hotfix above,
v0.1.1).
Tag every release on main and your team, along with your users, can always check out exactly what shipped and when.
The git flow CLI
You do not have to type all of those merges and tags by hand. The git flow extension wraps the whole model into a handful of commands.
git flow init # set up main, develop and the branch prefixes
git flow feature start contributing # branch off develop
git flow feature finish contributing # merge back into develop, delete the branch
git flow release start 0.1.0 # branch off develop
git flow release finish 0.1.0 # merge to main and develop, tag main
git flow hotfix start 0.1.1 # branch off main
git flow hotfix finish 0.1.1 # merge to main and develop, tag main
It is doing exactly the merges and tags you just learned, which is why it is worth understanding the manual flow first. One heads-up for 2026: the original nvie/gitflow extension was archived in October 2025 (it now points to a successor called git-flow-next), and the long-popular gitflow-avh fork was archived back in 2023. The model is the same either way, so I lean on the plain git commands above and treat the CLI as optional sugar.
Wrapping up
That is Git Flow end to end: two long-lived branches, supporting branches for features, releases and hotfixes, and a tag on main for every version. The structure shines when you ship versioned software on a schedule and have to maintain more than one release at a time.
If that does not sound like your project, do not force it. The real win is that your whole team agrees on how branches are named, when things merge, and how releases get tagged. For a continuously deployed web app, that agreement usually looks like GitHub Flow instead. Either way, pick one, write it down, and get everyone using it.