A Practical Guide to the Gitflow Workflow
A full walkthrough of the Gitflow branching model — feature, release, and hotfix branches, with the actual commands and the gotchas that bite people later.
If you've worked on a team project for more than a few weeks, you've probably hit the question: "Wait, where does this fix go — main, develop, or somewhere else?" Gitflow is one answer to that question. It's a branching model that gives every piece of work — features, releases, hotfixes — a defined home and a defined path back into production.
It's not the only way to use Git, and it's not always the right choice (more on that at the end). But understanding it deeply makes you better at reasoning about any branching strategy, because most alternatives are really just "Gitflow, but simpler."
This post walks through the model conceptually, then gives you the actual commands to use it day to day.
The Core Idea
Gitflow, introduced by Vincent Driessen in 2010, organizes work around two permanent branches and several types of temporary, purpose-built branches.
The permanent branches:
main— always reflects production. Every commit onmainis a released version.develop— the integration branch. This is where finished features accumulate before they're bundled into a release.
The temporary branches:
- Feature branches — for building new functionality. Branch off
develop, merge back intodevelop. - Release branches — for stabilizing a set of features before shipping. Branch off
develop, merge into bothmainanddevelop. - Hotfix branches — for emergency production fixes. Branch off
main, merge into bothmainanddevelop.
The pattern across all three: temporary branches are born from one branch and die after merging into one or two others. Nothing lives forever except main and develop.
Setting Up
Start by initializing develop alongside main:
git checkout main
git checkout -b develop
git push -u origin develop
From here on, develop is your default working branch — not main.
Feature Branches
Use these for any new functionality. They branch from develop and merge back into develop — never into main directly.
# Start a new feature
git checkout develop
git checkout -b feature/user-authentication
# Work normally
git add .
git commit -m "Add login form validation"
git commit -m "Wire up auth API endpoint"
# When the feature is done, merge it back
git checkout develop
git merge --no-ff feature/user-authentication
git push origin develop
# Clean up
git branch -d feature/user-authentication
That --no-ff flag matters. Gitflow relies on it almost everywhere. A normal merge can fast-forward and silently absorb your feature's commits into develop's linear history, erasing the fact that they were ever a separate branch. --no-ff forces a merge commit, so the feature's existence is preserved in the log — useful later if you need to find or revert the whole feature in one move.
If you're using GitHub, GitLab, or similar, you'd typically open a pull/merge request instead of merging locally, but the branch structure is identical either way.
Release Branches
When develop has accumulated enough features for a release, cut a release branch. This is where you do final testing, bump version numbers, and fix last-minute bugs — but you stop adding new features.
# Cut the release branch from develop
git checkout develop
git checkout -b release/1.2.0
# Bump version, fix small bugs found during QA
git commit -m "Bump version to 1.2.0"
git commit -m "Fix off-by-one error in pagination"
# When it's ready, merge into main and tag it
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git push origin main --tags
# Merge the same fixes back into develop
git checkout develop
git merge --no-ff release/1.2.0
git push origin develop
# Clean up
git branch -d release/1.2.0
Notice the double merge: into main and develop. This is the detail people forget, and it's the one that causes the most pain later. If you only merge into main, the bug fixes you made during the release branch never make it back into develop — so the next feature branch is built on code that doesn't have those fixes, and they can resurface in the next release.
Hotfix Branches
For when production is broken and you can't wait for the next scheduled release. Hotfixes branch from main (not develop, since develop may already contain unfinished, unreleased work) and merge into both main and develop.
# Branch from main, not develop
git checkout main
git checkout -b hotfix/1.2.1
# Fix the issue
git commit -m "Patch critical null pointer in payment flow"
# Merge into main and tag
git checkout main
git merge --no-ff hotfix/1.2.1
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git push origin main --tags
# Merge into develop too
git checkout develop
git merge --no-ff hotfix/1.2.1
git push origin develop
# Clean up
git branch -d hotfix/1.2.1
Same principle as releases: skip the develop merge and the fix quietly disappears from future work, only to reappear as a "new" bug down the line.
Putting It Together
A rough mental model of how branches relate:
main ──●─────────────●───●──────────────●──
\ \ \ /
release \ \ \ /
\ \ \──────────/ (release/1.2.0)
develop ──●───●───●───●───●───●──────────●──
\ \ \
feature \ \ \──● (feature/c, merged)
\ \
●───●───● (feature/a, merged)
main only ever moves forward via merges from release/* or hotfix/*. develop is the constant hub everything else orbits.
Is Gitflow Right for You?
Gitflow was designed in an era of scheduled, versioned software releases — think desktop apps or libraries with numbered versions. It earns its complexity when:
- You maintain multiple production versions simultaneously (e.g., supporting
v1.xandv2.xat once). - Releases are deliberate, scheduled events, not continuous.
- You need a clear, auditable separation between "in production" and "in progress."
It's often overkill when:
- You deploy continuously (multiple times a day). The release-branch ceremony adds friction without adding safety.
- You're a small team or solo developer. The overhead of
developplus three branch types can outweigh the benefit. - Your team prefers trunk-based development, where everyone commits to
main(often behind feature flags) and CI/CD handles the rest.
GitHub Flow and trunk-based development are the common simpler alternatives — both essentially strip Gitflow down to main plus short-lived feature branches, leaning on automated testing and feature flags instead of long-lived develop and release branches to manage risk.
The Takeaway
Gitflow's real contribution isn't the specific branch names — it's the idea that different kinds of work (new features, release stabilization, emergency fixes) have different risk profiles and deserve different branching rules. Even if you end up using a lighter-weight model in practice, that's the lesson worth keeping.