The Strategy You Think Is Best Might Be Wrong for Your Team
Git Flow isn’t a best practice. It’s one strategy, and it might be wrong for your team. I know that sounds provocative — Git Flow has been the default recommendation in blog posts and conference talks for over a decade. But defaults aren’t decisions. And choosing a branching strategy without understanding the trade-offs is how teams end up with repositories that look like a plate of spaghetti.
I’ve worked with teams that swore by Git Flow, teams that committed straight to main like cowboys, and teams that used GitHub Flow with the discipline of a Swiss watch factory. Each approach worked — for that team, at that time, with that product. The strategy that saved one team from chaos would have suffocated another.
So instead of telling you which one to pick, let me tell you three stories. Real teams, real problems, real branching decisions. By the end, you’ll know which story sounds most like yours.
Story One: The Release Train (Git Flow)
Picture a fintech company in Bangalore. Forty developers. Three product lines. Regulatory requirements that demand versioned releases with audit trails. They can’t just push to production whenever they feel like it — every release needs sign-off from compliance, QA, and the product team.
This team needs Git Flow.
Vincent Driessen published the Git Flow model back in 2010, and it was designed precisely for situations like this. Two long-lived branches: main holds production-ready code, and develop serves as the integration branch for the next release. Feature branches sprout from develop, get merged back when complete. When it’s time for a release, a release branch gets cut from develop, stabilized, tested, and finally merged into both main and develop. Hotfixes branch off main for critical production issues.
# Initialize Gitflow branches
git checkout -b develop main
# Start a feature
git checkout -b feature/user-authentication develop
# Work on the feature, then merge back
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authentication
# Create a release branch
git checkout -b release/1.2.0 develop
# Finalize and merge the release
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
git branch -d release/1.2.0
# Hotfix from main
git checkout -b hotfix/fix-login-crash main
# ... apply fix ...
git checkout main
git merge --no-ff hotfix/fix-login-crash
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git checkout develop
git merge --no-ff hotfix/fix-login-crash
git branch -d hotfix/fix-login-crash
Our Bangalore fintech team adopted this and it worked. Compliance could point to tagged releases on main. QA had a dedicated release branch to test against. Developers knew exactly where to create their branches and where to merge them. The structure gave everyone a shared vocabulary: “Is this on develop?” “When does 1.3 cut?” “Can we hotfix that?”
But here’s what happened over time. Feature branches started living for weeks. Sometimes a month. And long-lived feature branches are where Git Flow becomes painful. Merge conflicts multiply. Developers working on related features step on each other’s toes without realizing it until merge day. The integration step — merging a two-week-old branch back into develop — turned into a mini-crisis every sprint.
Two senior engineers spent roughly a day per sprint just resolving merge conflicts and fixing integration issues. That’s expensive. Not just in hours, but in morale. Nobody becomes a developer because they love resolving merge conflicts.
Git Flow works beautifully when branches stay short-lived. It struggles when they don’t. And in organizations with complex features that span multiple sprints, they often don’t.
The Bangalore team eventually adapted. They started breaking features into smaller, independently mergeable pieces. Instead of one massive feature/payment-redesign branch living for three weeks, they’d create five smaller branches — feature/payment-ui-scaffold, feature/payment-api-integration, feature/payment-validation — each merging into develop within a few days. It was more overhead in terms of planning, but merge conflicts dropped by roughly 70%. The key insight: Git Flow’s problem isn’t the model — it’s how teams use it. If you can keep branches small within the framework, it holds up well.
When Git Flow Fits
- Scheduled releases (monthly, quarterly, or version-based)
- Multiple production versions maintained simultaneously
- Regulatory or compliance requirements for release tracking
- Large teams where branch-type conventions reduce confusion
When It Doesn’t
- Continuous deployment environments
- Small teams (under 10) where the ceremony feels heavy
- Teams where features regularly take weeks to complete
Story Two: The Speed Demons (Trunk-Based Development)
Different team. A four-person startup in Pune building a SaaS product. They deploy multiple times a day. Their CI pipeline runs in under three minutes. Every developer on the team writes tests, reviews code, and deploys. No dedicated QA team, no release manager, no compliance officer looking over their shoulders.
Git Flow would have crushed them. All that branch management overhead for four people deploying continuously? Absurd. They’d spend more time managing branches than writing features.
They went with trunk-based development instead.
Trunk-based development is the opposite of Git Flow’s structured hierarchy. Everyone commits to a single branch — main or trunk. Short-lived feature branches are allowed, but “short-lived” means hours, not days. Definitely not weeks. The core principle is this: integration pain grows exponentially with branch lifetime. So you minimize lifetime by integrating constantly.
# Create a short-lived branch
git checkout -b feat/add-search-bar main
# Make small, focused commits
git add src/components/SearchBar.tsx
git commit -m "feat: add search bar component skeleton"
git add src/components/SearchBar.tsx src/hooks/useSearch.ts
git commit -m "feat: wire search bar to API hook"
# Rebase onto latest main before merging
git fetch origin
git rebase origin/main
# Merge quickly (ideally same day)
git checkout main
git merge --ff-only feat/add-search-bar
git push origin main
git branch -d feat/add-search-bar
Our Pune startup loved this. Code landed on main fast. Conflicts were rare because branches lived for hours, not weeks. The feedback loop was tight — write code, merge, see it in production within minutes. One developer described it as “the branches are so short they barely exist.”
But trunk-based development has a catch that’s easy to overlook. Incomplete features end up on main. You’re merging work-in-progress into the branch that deploys to production. That’s fine — as long as you have feature flags.
// Simple feature flag implementation
const FEATURE_FLAGS = {
newSearchBar: process.env.ENABLE_SEARCH_BAR === 'true',
darkMode: process.env.ENABLE_DARK_MODE === 'true',
};
function App() {
return (
<div>
<Header />
{FEATURE_FLAGS.newSearchBar && <SearchBar />}
<MainContent />
</div>
);
}
Feature flags let you merge code that’s not ready for users. The code is in production, but the feature is hidden behind a flag. Turn it on for internal testing. Turn it on for 10% of users. Turn it on for everyone. Or turn it off instantly if something breaks. It’s like having a release branch without the branch.
The Pune team ran into trouble exactly once, and it was instructive. A junior developer pushed code to main that passed all tests but had a subtle race condition that only appeared under load. Without a staging environment or a release branch as a buffer, the bug hit production within minutes of merging. They caught it fast and rolled back, but the incident exposed the fragility of trunk-based development without thorough automated testing.
After that, they invested heavily in their test suite. Integration tests, load tests, contract tests. Their CI pipeline grew from three minutes to eight, but broken code stopped reaching production. The trade-off was worth it.
There’s a lesson here that applies to trunk-based development universally: the strategy is only as safe as your test coverage. I’ve seen teams adopt trunk-based development because it sounds modern and fast, but they skip the hard work of building the testing infrastructure that makes it safe. Without that foundation, you’re just deploying untested code faster. That’s not agility. That’s recklessness with better branding.
The Pune team also added a monitoring layer — real-time error tracking with Sentry and uptime checks with Betteruptime. If a deployment caused a spike in errors, they got a Slack notification within 60 seconds. Paired with fast rollbacks (they could revert any deploy in under two minutes), this gave them the confidence to keep deploying multiple times daily even after the race condition incident. The safety net wasn’t the branch structure — it was the observability and rollback speed.
When Trunk-Based Development Fits
- Continuous deployment pipelines
- Small, experienced teams with strong testing discipline
- Products where speed-to-production matters more than release formality
- Teams comfortable with feature flags and incremental delivery
When It Doesn’t
- Teams without strong automated testing (this is non-negotiable)
- Organizations requiring formal release sign-offs
- Projects maintaining multiple production versions
- Teams with junior-heavy composition and no safety nets
Story Three: The Review Culture (GitHub Flow)
Third team. A mid-size agency in Delhi with twelve developers. They build web apps for clients, maintain several simultaneously, and care deeply about code quality. They’d tried Git Flow and found the ceremony excessive for their project sizes. They’d considered trunk-based development but felt uncomfortable merging without code review — too much risk on client projects where bugs had financial consequences.
GitHub Flow was their middle ground.
One long-lived branch: main. Everything else is a feature branch. Feature branches get merged via pull requests, which require at least one approval before merging. Once merged, the code deploys. Simple. Clean. Pull requests as the quality gate.
# Create a descriptive branch name
git checkout -b feature/improve-checkout-flow
# Commit your work
git add .
git commit -m "feat: redesign checkout step indicators"
# Push and open a pull request
git push -u origin feature/improve-checkout-flow
# Using GitHub CLI to create the PR
gh pr create \
--title "Redesign checkout step indicators" \
--body "Replaces the old numbered steps with a progress bar.
- Updated StepIndicator component
- Added animation between steps
- Mobile responsive layout"
# After approval, merge via the GitHub UI or CLI
gh pr merge --squash
# Clean up
git checkout main
git pull origin main
git branch -d feature/improve-checkout-flow
The Delhi agency thrived on this. Code review became their primary quality control mechanism, and the PR-centric workflow made it natural. Every change had a reviewer. Every reviewer left comments. Knowledge shared across the team constantly, almost as a side effect of the branching model itself.
New developers ramped up faster because reading PRs taught them the codebase and the team’s standards simultaneously. Senior developers caught architectural issues before they solidified. And clients could be shown PR descriptions as a log of what changed and why — a lightweight audit trail without Git Flow’s heavy structure.
Where GitHub Flow gets tricky is environments. The model doesn’t have a built-in concept of staging, UAT, or pre-production. Code goes from feature branch to main to production. If you need a staging step — and many client-facing agencies do — you have to layer it on top. The Delhi team added a staging branch that auto-deployed to a review environment, plus a convention where PRs tagged “needs-client-review” waited for client approval before merging to main. It worked, but it was a custom addition to the model, not part of it.
Another nuance: PR quality correlates directly with GitHub Flow’s effectiveness. When reviews are thorough — reviewers actually reading the code, running it locally, asking questions about design decisions — the model sings. When reviews become rubber stamps (a quick “LGTM” without real scrutiny), bugs slip through and the safety net has holes. The Delhi team addressed this with a “two-reviewer” rule for anything touching payment logic or authentication flows. Smaller UI changes needed one reviewer. This tiered approach kept reviews thorough where it mattered most without creating bottlenecks on low-risk changes.
They also invested in PR templates. Every pull request had a checklist: description of changes, screenshots for UI work, testing steps, and a note about which environments had been tested. Sounds bureaucratic, but it actually sped things up — reviewers spent less time asking “what does this do?” and more time evaluating whether it did it well.
When GitHub Flow Fits
- Teams that value code review as a core practice
- Open-source projects
- SaaS products with continuous deployment
- Small to medium teams (5-20 developers)
When It Doesn’t
- Projects needing formal release milestones
- Teams maintaining multiple simultaneous production versions
- Environments requiring multi-stage deployment pipelines (without customization)
The Practices That Work Regardless
Whichever story resonated with you, some habits keep your repository healthy no matter which branching model you follow.
Enforce commit message conventions. They feel bureaucratic until they save you an hour of archaeology trying to understand why someone changed a function six months ago.
# Enforce consistent commit messages with a hook
# .git/hooks/commit-msg
#!/bin/sh
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,72}'
if ! grep -qE "$commit_regex" "$1"; then
echo "ERROR: Commit message must follow conventional commits format."
exit 1
fi
Protect your main branch. Required status checks, required reviews, no force pushes. These rules feel restrictive when you’re in a hurry. They feel like life insurance when someone accidentally pushes a broken build.
# Protect your main branch with required status checks
gh api repos/:owner/:repo/branches/main/protection -X PUT \
-f "required_status_checks[strict]=true" \
-f "required_status_checks[contexts][]=ci/build" \
-f "required_status_checks[contexts][]=ci/test"
Clean up stale branches. Dead branches are clutter. They confuse new team members, make branch lists unreadable, and occasionally get mistaken for active work. Prune regularly.
# Prune stale remote branches regularly
git fetch --prune
git branch -vv | grep ': gone]' | awk '{print $1}' | xargs git branch -d
Write branch naming conventions and stick to them. feature/, fix/, hotfix/, chore/ — the specific prefixes matter less than consistency. When everyone on the team can glance at a branch name and know what kind of work it contains, collaboration gets smoother.
Quick-Reference Checklist: Which Strategy Fits Your Team?
Answer these five questions honestly. Your answers point to your strategy.
1. How often do you deploy?
- Multiple times per day –> Trunk-based or GitHub Flow
- Weekly or biweekly –> GitHub Flow
- Monthly or quarterly –> Git Flow
2. How large is your team?
- 1-5 developers –> Trunk-based
- 5-20 developers –> GitHub Flow
- 20+ developers –> Git Flow or GitHub Flow with conventions
3. Do you maintain multiple production versions?
- Yes –> Git Flow
- No –> Trunk-based or GitHub Flow
4. How strong is your automated testing?
- Strong (high coverage, fast CI) –> Trunk-based
- Moderate –> GitHub Flow
- Weak or nonexistent –> Git Flow (the branch structure provides safety nets)
5. How important is code review to your team?
- Non-negotiable –> GitHub Flow
- Nice to have –> Any strategy with PR requirements
- We skip it (no judgment… okay, a little judgment) –> Trunk-based with strong CI
If your answers point in different directions, lean toward the strategy that matches your deployment frequency. That’s usually the strongest signal. A team deploying daily will chafe under Git Flow’s ceremony, and a team shipping quarterly will feel exposed without it.
And one last thing: revisit your strategy as your team grows. The branching model that worked for five people won’t necessarily work for twenty-five. That’s not failure. That’s evolution. The best branching model isn’t the one with the most impressive diagram. It’s the one your team actually follows, consistently, without wanting to throw their laptops out the window.