A Practical Guide to Git Branching Strategies
Most teams don't choose a branching strategy — they inherit whatever the founding engineers happened to do, and it calcifies before anyone questions whether it fits the team's actual size and release cadence. That's how you end up with a five-person startup running a process designed for a hundred-person org with quarterly releases, or a team shipping to production daily still maintaining a permanent develop branch nobody remembers the purpose of. The right strategy depends entirely on how often you deploy and how many things are in flight at once — not on which one has the fanciest diagram.
Trunk-based development
Everyone commits to main (or very short-lived branches merged within a day), behind feature flags if the work isn't ready to be seen by users yet.
git checkout main
git pull
git checkout -b add-export-button
# ... small, focused change ...
git push -u origin add-export-button
# open PR, merge same day, delete branch
This works well when you deploy frequently — multiple times a day, or at least daily — and have solid CI and feature flagging. The whole point is that branches never live long enough to drift far from main, so merge conflicts stay small and integration problems surface immediately instead of piling up. It falls apart without feature flags: if "small, focused change" quietly becomes "half-finished feature visible to users," you've just reinvented a long-lived branch with extra steps.
Git Flow
The classic heavyweight model: a permanent develop branch for integration, feature/ branches off develop, release/ branches for stabilizing a version, and main reserved for what's actually in production, with hotfix/* branches for patching it directly.
git checkout develop
git checkout -b feature/user-avatars
# ... work, potentially over days or weeks ...
git checkout develop
git merge --no-ff feature/user-avatars
# when develop is ready to ship
git checkout -b release/2.4.0 develop
# stabilization fixes go here, then:
git checkout main
git merge --no-ff release/2.4.0
git tag v2.4.0
git checkout develop
git merge --no-ff release/2.4.0
This makes sense when you genuinely ship on a schedule — a desktop app with quarterly releases, an embedded firmware update, anything where "what's currently in main" and "what we're actively building toward" need to be different things for weeks at a time. For a web app deploying continuously, the develop/release split adds ceremony without a corresponding benefit — there's no separate "release" moment to stabilize toward.
GitHub Flow
The middle ground: no develop, no release branches. main is always deployable, feature branches come straight off it, and merging to main is what triggers a deploy.
git checkout main
git pull
git checkout -b fix-pagination-off-by-one
# ... work ...
git push -u origin fix-pagination-off-by-one
# open PR, get review, merge — CI deploys main automatically
This is the default a lot of teams should reach for: it's Git Flow's clarity (a real branch per unit of work, reviewed via PR) without the multi-branch bookkeeping, and it fits continuous deployment naturally since there's exactly one branch that matters. The tradeoff versus trunk-based development is branch lifetime — a feature branch here might live for a few days of review and iteration, which reintroduces some merge drift that pure trunk-based avoids, but in exchange you get an actual review checkpoint before code reaches main.
Picking one
- Trunk-based: deploying multiple times a day, have feature-flag infrastructure, team is disciplined about small commits.
- GitHub Flow: deploying at least a few times a week, want PR review as a gate, don't need parallel release lines.
- Git Flow: shipping discrete, numbered releases with real time between them, need to patch an older version while developing the next.
Team size matters less than people assume. A solo developer benefits from GitHub Flow's PR-per-change discipline as documentation of intent, even with no one else to review it. A twenty-person team can run trunk-based development successfully if the CI and flagging investment is there — the constraint is process maturity, not headcount.
Naming conventions that actually help
Whatever strategy you pick, a consistent branch prefix makes git branch -a and CI dashboards scannable at a glance:
feature/short-description
fix/short-description
chore/short-description
hotfix/short-description
Skip embedding ticket numbers as the only identifier (JIRA-4521) — six months later nobody remembers what that was without looking it up, whereas fix/export-csv-encoding tells you immediately. If your tracker matters for automation, put the ticket ID after the description, not instead of it: fix/export-csv-encoding-JIRA-4521.
The one habit that matters more than the strategy
Regardless of which model you use: merge or rebase from the base branch before opening a PR, not after review comments start rolling in on a now-stale diff.
git checkout feature/add-export-button
git fetch origin
git rebase origin/main
# resolve conflicts here, while the change is fresh in your head
git push --force-with-lease
--force-with-lease instead of --force matters here — it refuses to overwrite the remote branch if someone else has pushed to it since you last fetched, which plain --force would silently clobber.
Takeaway
There's no universally correct branching strategy — there's only the one that matches your actual deploy frequency. Trunk-based and GitHub Flow both assume main is close to always-shippable and differ mainly in how much review friction sits before a merge; Git Flow assumes it isn't, and pays for that with more branches to track. Pick based on how often you ship, not on which diagram looks most sophisticated in the wiki.