Git-flow
Git-flow keeps two long-lived branches — main (released) and develop (integration) — plus short-lived feature/, release/, and hotfix/ branches. It excels at versioned releases and multiple live versions, but its many branches make it heavy for CD; legacy for web apps.
In 2010 Vincent Driessen published “A successful Git branching model,” and for a decade it was the answer when a team asked “how should we use branches?” It has a name — git-flow — a diagram everyone has seen, and even a set of helper commands. It is also the most structured, most ceremonious workflow you will meet, with five kinds of branch and strict rules for how each one merges.
By the end of this lesson you will be able to trace a feature, a release, and a hotfix through git-flow’s branch structure, and say precisely which kind of project it is still the right choice for — and which kind it actively slows down.
After this lesson you can name git-flow’s two long-lived branches and three supporting branch types, trace how a feature, a release, and a hotfix each move through them, and explain why git-flow fits versioned releases but fights continuous delivery.
Git-flow has two long-lived branches: main and develop. main holds only released, production code — every commit on it is a shipped version, usually tagged (v1.4.0). develop is the integration branch: it accumulates finished features that are headed for the next release but are not shipped yet. This split is the heart of git-flow. main answers “what is in production,” develop answers “what will be in the next release,” and the two are never the same branch. Everything else in git-flow is a short-lived branch that eventually merges into one or both of these.
Features branch off develop and merge back into develop. A feature/* branch is where one unit of new work happens. It starts from the current tip of develop and, when reviewed and done, merges back into develop — never directly into main.
git checkout develop
git checkout -b feature/user-search
# ... commits ...
git checkout develop
git merge --no-ff feature/user-search
git branch -d feature/user-searchThe --no-ff (no fast-forward) flag is idiomatic here: it forces a merge commit so the feature stays visible as a grouped unit in history rather than being flattened into develop. Features never touch main directly — production code only ever arrives through a release.
A release branch stabilises develop, then merges into BOTH main and develop. When develop has enough features for a release, you cut a release/* branch off it. On that branch you do only release work — version bumps, last bug fixes, changelog — but no new features. When it is ready, it merges into main (and gets tagged with the version), and also back into develop so the late fixes are not lost:
git checkout develop
git checkout -b release/1.5.0
# bump version, fix release-only bugs ...
git checkout main
git merge --no-ff release/1.5.0
git tag -a v1.5.0 -m "Release 1.5.0"
git checkout develop
git merge --no-ff release/1.5.0
git branch -d release/1.5.0The double merge is the part people forget. Skip the merge back into develop and your release-only fixes vanish from the next development cycle.
Hotfixes branch off main, because that is where production lives. A production-critical bug cannot wait for the next release cycle through develop. A hotfix/* branch starts from main itself, fixes the one thing, then — like a release — merges into both main (tagged as a patch, v1.5.1) and develop (so the fix is not undone next release):
git checkout main
git checkout -b hotfix/1.5.1
# fix the bug ...
git checkout main
git merge --no-ff hotfix/1.5.1
git tag -a v1.5.1 -m "Hotfix 1.5.1"
git checkout develop
git merge --no-ff hotfix/1.5.1This is git-flow’s real strength: a clean, separate path for urgent production fixes that never drags in unfinished develop work.
▸Why this works
The original blog post shipped a git flow command-line extension (git flow init, git flow feature start, git flow release finish) that automates exactly these branch-and-merge sequences. The helper is convenient but optional — git-flow is a convention, and every command above is plain git. The risk of leaning on the helper is that engineers learn the wrapper without understanding the underlying merges, then panic when something is off-script. Knowing the raw operations is what lets you recover when the wrapper does not fit.
Shipping 1.5.0, then patching it the same day.
Your develop has three merged features. Friday you cut release/1.5.0, bump the version, and QA finds one bug on the release branch — you fix it there. Monday the release merges into main, you tag v1.5.0, deploy, and merge release/1.5.0 back into develop so that Friday’s fix lives on. Tuesday a customer hits a crash in production. You do not touch develop (which already has new 1.6 features half-done). Instead you branch hotfix/1.5.1 off main, fix the crash, merge to main, tag v1.5.1, deploy, and merge the hotfix into develop too.
Notice what git-flow bought you: the in-progress 1.6 work on develop never came near production, the hotfix shipped from exactly the code that was live, and both the release fix and the hotfix were preserved for the next cycle. That clean separation of “what is live” from “what is next” is the whole reason git-flow exists.
▸Common mistake
Git-flow’s weakness is the flip side of its strength: it is heavy. Two long-lived branches plus three supporting types mean constant cross-branch merging, and the develop-then-release-then-main path adds latency between “feature done” and “feature live.” For a web app that deploys many times a day, that ceremony is pure overhead — there is no “version 1.5.0,” there is just main and production. Driessen himself added a 2020 note saying git-flow was designed for versioned software and that teams doing continuous delivery should use a simpler model. Reaching for git-flow on a continuously-deployed web app is the single most common workflow mistake.
In git-flow, where does a hotfix branch start from, and why?
Git-flow uses two long-lived branches — main for released code and develop for integration — plus short-lived feature/, release/, and hotfix/ branches. Features flow through develop; a release branch stabilises then merges into both main (tagged) and develop; a hotfix branches from main and likewise merges into both. Its strength is clean support for versioned releases and multiple production versions; its weakness is the weight of all those long-lived branches, which fights continuous delivery. That is why it is increasingly legacy for web apps and best kept for libraries, desktop, and firmware. Next you will see its near-opposite: GitHub flow, with a single long-lived branch.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.