Most teams inherit a branching model by accident—copying a blog post from 2015 or mirroring a previous employer. Pick a model on purpose. For small teams (roughly 3–15 engineers), the winning criteria are usually: review speed, main always deployable, and rollback clarity—not ceremonial branch purity.
Decision frame
Ask four questions before choosing:
- Do you ship one production line or maintain multiple released versions?
- How strong is CI (tests, lint, preview deploys)?
- Can you use feature flags for unfinished work?
- How often do you need hotfixes under pressure?
One prod version + solid CI + flags?
└─ Yes → Trunk-based or GitHub Flow
└─ No, multiple supported releases → Git Flow (or release branches)
Weak CI?
└─ Fix CI first; branching ceremony will not save youTrunk-based development
Keep main (trunk) releasable. Engineers open short-lived feature branches (hours to a couple of days), merge small PRs, and hide incomplete behavior behind flags.
main ─●──●──●──●──●──●──●──► (always green, frequently deployed)
\ \ /
a b c ← branches live brieflyPros: fewer merge hells, faster feedback, simpler continuous delivery.
Cons: requires discipline on flag cleanup and test quality.
Info: Trunk-based development works best when you have solid CI and small pull requests. A three-thousand-line PR is not trunk-based; it is a weekend hostage situation.
Feature flag sketch
if (flags.isEnabled("new-checkout", user)) {
return renderNewCheckout();
}
return renderLegacyCheckout();Delete the flag when the rollout hits 100% and metrics look sane—otherwise trunk becomes a graveyard of permanent conditionals.
GitHub Flow
A lightweight relative of trunk-based practices, popularized for web apps:
- Branch from
mainwith a descriptive name (feat/invoice-pdf) - Open a pull request early (draft PRs welcome)
- Review, ensure CI green, merge
- Deploy
main(auto or one click)
main ──●──────────●──────────●──►
\ /
●─●─●─● pull/123 (reviewed)Pros: easy to teach; maps cleanly to GitHub/GitLab UX.
Cons: long-lived branches creep back in without norms; still needs release discipline.
PR hygiene checklist
- Keep diffs near 400 lines or fewer when possible (split vertical slices)
- Description includes risk + test plan
- Migrations are expand/contract safe
- No “WIP commit spam”—squash or tidy before merge if your team prefers
- Preview environment exercised for UI changes
When Git Flow still helps
Git Flow adds long-lived develop, release/*, and hotfix/* branches. It shines when you support multiple production versions (mobile store binaries, on-prem yearly releases) or when QA needs a frozen release candidate.
main ────●────────────●────► (tagged releases)
↑ ↑
release/1.2 ─●──●──●───────┘
↑
develop ─●──●─┴──●──●──●──────►
\
hotfix ────●──────────────────►Pros: clear names for release hardening.
Cons: merge overhead; slow teams often drown here. Small SaaS teams rarely need full Git Flow.
Hotfixes without drama
Whatever model you use, document one hotfix path:
git switch -c hotfix/payment-timeout main
# fix, test, PR into main
# if you maintain a release branch, cherry-pick or merge back
git tag -a v1.4.1 -m "payment timeout guard"Tag what you deployed. “We think prod is commit X” is not a release process.
Real team mistakes
| Mistake | Symptom | Repair |
|---|---|---|
| Week-long feature branches | Endless rebases | Smaller slices + flags |
Direct commits to main | Broken deploys | Branch protection + required checks |
| “Release branch” for every sprint | Merge tax | Prefer tags from main |
| Rewriting shared history | Lost work, force-push wars | Ban force-push on shared branches |
| No CODEOWNERS | Random review quality | Own critical paths |
| Hotfix only on prod box | Undocumented drift | Always land fix in git first |
BROKEN CULTURE HEALTHY CULTURE
────────────── ───────────────
"Don't touch main" "Main is sacred and busy"
Giant monthly merges Daily small merges
Fear of deploy Deploy is boring
Flags never removed Flag tickets have expirySuggested default for small product teams
If you run a single SaaS product with decent CI:
- Protect
main(reviews + required checks) - Use short-lived branches (GitHub Flow / light trunk-based)
- Deploy
mainoften - Use flags for incomplete features
- Tag releases; skip full Git Flow until multi-version support forces it
Naming and protection rules that prevent chaos
Agree on branch prefixes (feat/, fix/, chore/, hotfix/) and delete remote branches after merge. Turn on:
- required status checks on
main - required review from code owners for sensitive paths (
/infra,/auth) - no force-push on default and release branches
- linear history or merge commits—pick one and document it
# Example local habit: rebase only your unshared branch
git fetch origin
git rebase origin/main
git push --force-with-lease # never --force on shared branches--force-with-lease refuses to overwrite work you have not seen; plain --force is how teammates lose commits.
Migrations and branching
Schema changes break branching models when expand/contract discipline is missing. Prefer additive migrations first (expand), deploy code that reads both shapes, then remove the old shape (contract) in a later PR. A long-lived branch that mixes irreversible migrations with feature work is how you get undeployable main.
Closing tip
Optimize for review speed and rollback clarity—not ceremony. A branching strategy is successful when new hires can ship safely in week one and on-call can answer “what is in prod?” in ten seconds. Write the rules in a one-page CONTRIBUTING.md, then prune every process that does not serve those two outcomes.
