The 5 Rules of Git Branching for High-Velocity Teams
Practical constraints for code integration that actually work. Less about naming conventions, more about not breaking production.

More engineering hours have been wasted arguing about branching strategies than any branching strategy has ever saved. That's my working theory, anyway. Meetings about naming conventions, whether develop should exist, what "release branches" look like, who can push where โ the collective heat generated by these conversations is rarely proportional to their impact on whether a team ships well or doesn't.
Here is what I've observed across a decade of working on teams ranging from two people to forty-plus engineers in the same monorepo: the branching model matters less than whether the team agreed on one and stuck with it. A mediocre strategy followed consistently beats a theoretically perfect one that half the team ignores and the other half interprets differently.
That said โ opinions exist. Some strong, some less certain. Different team sizes and deployment models really do need different approaches, and anyone telling you there's one correct answer is selling a conference talk. So instead of rules, I want to walk through the main approaches, say what I think about each, describe what I actually do, and let you decide what maps to your situation.
GitFlow: The Diagram That Launched a Thousand Arguments
Vincent Driessen published the GitFlow model in 2010. Two long-lived branches โ main and develop. Feature branches come off develop, merge back in. Release branches get cut from develop, stabilized, then merged into both main and develop. Hotfix branches come off main for emergency patches.
Beautiful on a whiteboard.
In practice, every team I've been on that tried GitFlow spent noticeable standup time discussing merge logistics rather than product work. The ceremony is heavy. You end up with constant bookkeeping โ making sure merges land in the right places, that develop and main haven't drifted apart in confusing ways. I've been on a team where develop was three weeks ahead of main and nobody could say with confidence what was actually running in production.
Where GitFlow still makes sense: versioned software. Desktop applications. Mobile apps with App Store review cycles. Libraries that maintain multiple supported versions simultaneously. The release branch concept maps to a real thing in those contexts โ you need a place to stabilize a release while new feature work continues in parallel.
Web application that deploys multiple times a day? GitFlow adds process without adding value. I've watched teams adopt it because it seemed "professional" and then spend their energy on branch coordination instead of building features.
One thing I'll give GitFlow credit for โ it forces people to think about what "a release" means. Teams that skip straight to trunk-based development sometimes lose that discipline entirely. Not sure that's always better. More on that in a moment.
GitHub Flow: Stripped Down and Usually Sufficient
main is always deployable. Make a branch. Do work. Open a pull request. Get it reviewed. Merge. Deploy. Delete the branch. That's GitHub Flow.
git checkout -b add-invoice-export
# do work, commit, push
git push -u origin add-invoice-export
# open PR, get review, merge, deploy
For most web teams, this is enough. Easy to explain to a new hire in five minutes. No ambiguity about where code goes or what state any branch is in. main represents production. Everything else is work in progress.
The thing that makes or breaks GitHub Flow is CI. If merging to main triggers a deploy, your pre-merge checks need to be solid. Tests. Linting. Type checking. Build verification. Because there's no staging branch, no release stabilization period โ nothing between "PR approved" and "code running in production."
Some teams bolt on environment branches anyway โ staging, qa, whatever โ and at that point you're kind of reinventing parts of GitFlow but without the formal structure. Done this. Works fine. A bit conceptually messy, but functional.
The honest downside: GitHub Flow assumes your PRs are small and your CI pipeline is fast. When a PR takes four days to review and another day to get through a slow pipeline, branches go stale and merges get painful regardless of what you call your strategy.
Trunk-Based Development: The One That Makes People Nervous
Everyone commits to main. Either directly, or through branches that live for hours, not days. Continuous integration in the literal sense โ code integrates with everyone else's code constantly.
Google does this at scale. Many high-performing teams practice it. And it makes a lot of engineers uncomfortable, because "everyone pushes to main" sounds like chaos if you've been burned by broken builds hitting production.
Trunk-based doesn't mean "push whatever and hope." It means:
- Strong automated testing before and after every commit
- Feature flags to hide incomplete work from users
- Small, incremental changes instead of big-bang feature branches
- Post-commit or pair-programming review instead of (or alongside) pre-merge PR review
# A typical TBD workflow
git pull --rebase origin main
# make a small, focused change
git add -p
git commit -m "Add currency formatting to invoice totals"
git push origin main
Or with short-lived branches:
git checkout -b anurag/currency-format
# a few hours of work, maybe a day at most
git push -u origin anurag/currency-format
# quick review, merge same day, delete branch
Probably the right answer for most experienced web teams. Probably. I hedge because I've also seen it fail badly when the testing infrastructure wasn't there to support it. If your test suite runs for forty minutes and flakes on every third run, trunk-based development will hurt. Broken builds. Red main branch half the time. People afraid to push.
Feature flags carry real cost too. You need a system for managing them, cleaning up old ones, testing different flag combinations. I've seen codebases where dead feature flags accumulated for years because nobody wanted to risk removing them. Technical debt wearing a different mask.
But when the preconditions are met โ fast tests, managed flags, disciplined team โ the speed is difficult to beat. Merge conflicts essentially disappear because nothing has time to diverge. Integration pain drops to near zero.
The Things That Matter Regardless of Which Model You Picked
Whatever you choose, some principles hold across all of them. These aren't branching strategy decisions. They're just git hygiene that every team should practice.
Protect Main
Absolute position on this one. main is broken, everything downstream breaks. CI gates on merges. Nobody pushes directly without checks passing. Branch protection rules on GitHub take five minutes to configure and cost nothing. There is no excuse for leaving main unprotected.
# A basic GitHub Actions check
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm run build
I wrote about a real incident where missing branch protection led to a force-push disaster in my post-mortem on a disastrous git merge. Been on exactly one team that decided protection rules were "too much process" and that everyone was senior enough to self-police. Lasted about three weeks before someone pushed a syntax error to main on a Friday afternoon and the on-call person spent their evening fixing it. Protection rules went on the following Monday.
Smaller Changes, More Frequently
Don't care what branching model you use. The single highest-impact practice is making changes smaller. A 50-line PR gets reviewed in ten minutes and merges cleanly. A 2,000-line PR sits in review for a week, accumulates nitpick comments, and becomes a merge conflict disaster.
Harder than it sounds. Breaking a large feature into small, independently shippable pieces requires thinking about work differently. Not "build the whole thing and submit it" but "what's the smallest change that moves us forward and leaves the codebase working?" Sometimes that means shipping backend changes before the frontend is ready. Sometimes it means adding a database migration in one PR and the code that uses it in another. Sometimes it means merging code that's technically dead until a flag gets flipped.
Feels slower while you're doing it. Faster overall. Convinced of this even though I still catch myself building too much before opening a PR.
The Rebase vs. Merge Question
Gone back and forth on this more than I'd like to admit. For years: rebase purist. Linear history, clean graph, git bisect works perfectly. Got annoyed at merge commits cluttering the log.
These days, less rigid. Here's where things settled:
For keeping a feature branch current with main:
git fetch origin
git rebase origin/main
Keeps your commits on top. History stays clean. Merge is straightforward when the time comes.
For the actual merge into main, squash-merge is usually the right call:
# On GitHub, just hit "Squash and merge" on the PR
# Or manually:
git checkout main
git merge --squash feature/invoice-export
git commit -m "Add invoice export to PDF and CSV formats"
Your main log becomes one commit per PR, which maps to one logical change. Detailed commit history still exists on the branch if anyone needs to look at it.
But โ squash merging does lose information. Ten careful, well-structured commits on a feature branch get flattened into one. For complex changes, that granular history can be valuable months later when someone is trying to understand why things were done in a certain sequence. No clean answer. For most daily work, squash-merge is fine. For large, carefully structured branches, maybe a regular merge is better. Use judgment. Unsatisfying advice, but I think it's correct.
Write Commit Messages That Help Future You
Not going to belabor this. A good commit message saves future-you real time. "fix bug" tells nobody anything. "Fix off-by-one error in pagination that caused last page to show duplicate items" tells you exactly what happened and why, six months from now when you're bisecting a regression.
Conventional commits format (feat:, fix:, chore:) works if your team likes it. Used on some projects, not others. The format matters less than writing an actual description.
Automate the Forgettable Stuff
Pre-commit hooks. CI checks. Automated formatting. Anything a human will eventually forget to do manually under time pressure.
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.css": ["prettier --write"]
}
}
"Please remember to run the linter before pushing" works until it doesn't. Usually fails during a crunch when people are rushing. Automation removes the question entirely.
Release Management
One thing that gets lost in the branching model debate: how do you mark what shipped? Continuous deploy from main means the latest commit is the release. But when you need to track versions โ customer asks "which version am I on?" or you need to reference specific points in time โ git tags work:
git tag -a v2.4.1 -m "Release 2.4.1 - invoice export and payment retry"
git push origin v2.4.1
Lightweight, don't require extra branches, give you permanent markers. Used even on continuous-deploy projects because "this bug was introduced between v2.4.0 and v2.4.1" is useful when debugging.
Some teams cut release branches within a GitHub Flow setup โ freeze what's going to production, do final testing on the branch, tag and deploy from it while new work continues on main. Reasonable middle ground. Not pure GitHub Flow. Not GitFlow. Something practical between the two.
Being pragmatic about mixing approaches is fine. Blog posts describe idealized workflows. Real teams adapt them. Normal and healthy.
Monorepos Change the Math
Quick aside because it comes up. Monorepo with multiple services or packages โ branching gets more complicated because a single branch might touch code owned by different teams.
Trunk-based development tends to work better in monorepos, from what I've seen. Long-lived feature branches in a monorepo produce merge conflicts of horrific scope, because so many people are changing so many things in the same tree simultaneously.
TBD in a monorepo requires good tooling, though. CI system that figures out what changed and only runs relevant tests. Code ownership rules. Nx, Turborepo, Bazel โ these tools exist for a reason.
Less experience here than with single-app repos, so I won't push hard on recommendations. Just noting that monorepo dynamics change the calculus in ways worth thinking about before picking a model.
What I Actually Use
Current projects: something that's basically GitHub Flow with short-lived branches and squash merges. Branches live one to three days max. CI runs on every push to a PR. Merging requires passing checks and at least one approval. Squash-merge nearly everything. Tag releases.
No develop branch. No release branches. Feature flags for bigger changes that ship incrementally.
Best approach? For this team size, this deployment model, this risk tolerance โ seems right. For a team shipping a mobile SDK to thousands of developers who pin to specific versions? Probably not. For a two-person startup moving as fast as possible? They could get away with even less process.
Point isn't finding the universally correct branching strategy. Find one that fits how your team works. Agree on it. Write it down somewhere. Stop arguing about it. Same pragmatism applies to other team-level decisions like whether to go microservices or stay with a monolith โ pick what fits, commit, revisit when it hurts. Revisit it if real pain shows up. But don't overhaul your workflow because someone published a blog post about how trunk-based is the only true way.
I've changed my mind on branching approaches three or four times over the past decade. Probably will again. The tools evolve, team size changes, deployment story changes. What felt right for five people might feel completely wrong for thirty.
Code Ownership and Branch Permissions
One thing that gets overlooked in branching discussions: who can merge what?
On our team, anyone can open a PR against main. But merging requires passing CI checks and at least one approval from a code owner. We use GitHub's CODEOWNERS file to map directories to responsible teams โ changes to the payment module require approval from someone on the payments team, changes to the authentication flow require someone from the platform team.
This intersects with branching because it affects how quickly PRs move. A PR that touches a single team's code gets reviewed and merged within hours. A cross-team PR โ touching files owned by two or three different teams โ can sit for days waiting for approvals. This creates an incentive to keep changes small and focused within one domain, which is a positive side effect of code ownership rules.
The downside: sometimes a refactoring that should be one PR has to be split into three because it crosses ownership boundaries. Each sub-PR has to make sense on its own, which requires careful sequencing. Extra work. But the alternative โ one massive cross-cutting PR that needs five approvals โ is worse.
We also restrict who can create release tags. Only the team lead and the release engineer can tag a version. This prevents accidental tags from triggering the deployment pipeline. Small restriction. Prevents a specific category of "oops" that happened once before the restriction existed.
My Final Take
Pick something reasonable. Protect main. Keep branches short. Automate what you can. Spend your meeting time on things that actually affect your users. Branching model is plumbing. Important plumbing โ but plumbing.
Written by
Anurag Sinha
Full-stack developer specializing in React, Next.js, cloud infrastructure, and AI. Writing about web development, DevOps, and the tools I actually use in production.
Stay Updated
New articles and tutorials sent to your inbox. No spam, no fluff, unsubscribe whenever.
I send one email per week, max. Usually less.
Comments
Loading comments...
Related Articles

Post-Mortem: A Disastrous Git Merge and the Resulting Workflow
A detailed breakdown of a force-push incident that deleted two days of work, and the strict git branching policies established in the aftermath.

Contributing to Open Source โ From First-PR Anxiety to Merged Code
How I got past the fear of my first pull request, found projects worth contributing to, and learned to read unfamiliar codebases without drowning.

Design Patterns in JavaScript โ The Ones That Actually Show Up in Real Code
Forget the Gang of Four textbook. These are the patterns I see in production JavaScript and TypeScript codebases every week โ observer, factory, strategy, and the ones nobody names but everyone uses.