#26 - Git Branching Strategies. Do's and Don'ts
Long-lived branches can accumulate the integration risk they were meant to reduce.
18 min read
Johan Abildskov(see episode 6) is back, and we are talking branching strategies! In particular, why you shouldn’t be doing git-flow, and what are other options out there. This conversation takes us down memory lane to a more broad discussion about version control systems, mono-repositories, continuous integration, and delivery. We hope you will like it!
The branching model you copied is slowing your releases down
Look inside a repository you didn’t set up and you’ll usually find the same five kinds of branch: master, develop, feature/*, release/* and hotfix/*. Nobody chose that layout this quarter. Someone read “A successful Git branching model” years ago, the diagram looked thorough, and the branches outlived everyone who agreed to them. What’s left is a team that asks around the office to find out what’s on develop. There’s a merge big enough to need its own sprint, and no single branch where green means the product works.
The author added a note of reflection to the top of that post on March 5, 2020. The note says the model was conceived in 2010, and that teams delivering continuously should adopt a much simpler workflow. It closes with a line most readers never got to: “always remember that panaceas don’t exist. Consider your own context.” Ten years on, plenty of teams still run the 2010 model and have never done that second part. The cost isn’t cosmetic. Your branching model sets how often your team integrates. Integration frequency sets how fast you can ship with confidence, which makes this a delivery decision wearing a version control costume.
Johan Abildskov has spent years teaching Git to engineers moving off older version control systems, as a consultant at Praqma, the Nordic consultancy Eficode acquired in May 2019. He wrote Practical Git (Apress, November 2020) and maintains the Git Katas exercise set. Git isn’t a tool he finds tremendously interesting, and he thinks it shouldn’t be. It’s one of those things you invest in once so it can stop mattering. Teams that skip that investment pay for it every week instead.
Branching postpones risk instead of removing it
Start with why the branches are there at all, because the stated reason and the actual effect are rarely the same.
Why does merging get worse the more carefully you branch?
Because isolation is deferred integration: the conflicts are still coming, there are just more of them and they all arrive on the same day.
Cheap branching is what sold Git, though the comparison was never about the cost of making a branch. In Subversion, a branch is a copy on the server, “a constant-time operation” in the Subversion book’s own words. What that book warns about is isolation rather than branching. Work alone in a private copy, without committing or updating, and the cost lands later. Other people’s changes become “difficult to incorporate into your working copy when you eventually run svn update after weeks of isolation”. Branching is what it recommends instead, not the thing it measures. The arithmetic carries over anyway: the longer you stay out of sync, the more there is to reconcile when you come back. ClearCase charged earlier and harder. You branched per object, so every directory and every file could sit on its own branch, selected by a config spec too large to write by hand. One organization we worked in had a person whose full-time job was merging branches, supervising scripts that walked directory by directory and file by file. Git made the whole cycle cheap, and that one property became a proxy for everything else the tool could do. Hand people a hammer called branches and they’ll go looking for nails.
So the friction went away, and the reasoning didn’t get re-examined. We branch to reduce risk. What the branch does is move the risk later and let it compound while it waits.
The failure mode is self-reinforcing. Merging hurts, so we conclude we haven’t separated things carefully enough, become more adamant about release branches and more willing to delay, and the next merge is worse. Ask a team using feature branches why, and the answer is to make merging safer. Then ask how merging feels, and you get “it’s always hell”, a merge sprint on the calendar, and people walking the office asking what’s actually on main.
Martin Fowler’s Patterns for Managing Source Code Branches (May 2020) names the distinction we keep losing. Pulling mainline into your branch isn’t integrating. Integration happens only when your work goes back where everyone else gets it.
You branch because verification is slow
There’s a force behind every branch, and pointing at discipline misses it.
What would have to change for your team to stop branching?
Your regression suite would have to finish fast enough that a mistake surfaces before you’ve written the next one.
If every regression test you own ran in under a second, you’d push to the same branch all day. A break would be obvious while you still remember what you typed. When the suite takes a week, piling changes onto the branch you release from is playing with fire. Something will break, and working out which of forty changes did it is the expensive part. People branch because of the second case. The branch is a symptom.
The other force is ownership. It’s my code, don’t touch my code. Then the code sits on a branch for half a year and comes back as a small-looking change that’s impossible to integrate.
You don’t ship from a feature branch. Green there buys a little confidence before you merge, and you still have to make it work on the mainline, where the only result that matters gets produced. Worse, every long-lived branch competes for the same test resources: time spent qualifying branches isn’t spent keeping the mainline shippable. Testing in isolation and calling it verified is a self-issued safety certificate.
In one embedded organization we worked in, the fix was procedural. We controlled the repository hosting, so organization-wide feature branches could be refused outright. Getting one meant going to the head of the department to explain why you needed it. That policy sounds bureaucratic and worked beautifully: people asked only when they genuinely needed a branch.
Whether a short feature branch is fine at all splits us. Mattias branches a feature, merges it a day or two later, maybe with an extra test and a second pair of eyes on the way, and calls that normal. Andrey’s position is blunter: the best way to use branches is not to use branches. Johan sides with the strict rule, and his reason isn’t about Git. A short-lived branch is a sane way to work. Most teams don’t hold the discipline that keeps it short. Telling people no branches and then granting the exceptions is easier than policing a limit nobody wrote down.
If you want a number rather than a policy, DORA’s analysis of its 2016 and 2017 State of DevOps data offers one. It ties higher delivery and operational performance to teams with three or fewer active branches, merged to trunk at least once a day. That’s research rather than our own experience, and it points the same way.
What git flow costs you
Which brings us back to the model everyone copied, and to what it charges per change.
What exactly is wrong with git flow?
The number of transitions a change has to survive. In the model as published, your work merges into develop and waits for a release branch to be cut. That branch then merges into master and back into develop.
Every one of those transitions is a place work can sit and a place two changes can collide. The release merges move a whole batch of features at once, which is when you least want a surprise. The model also assumes every developer is fluent in Git, because its happy path is made of merges. That assumption fails quietly in exactly the teams that adopted it for safety. Push continuous integration and delivery through that many gates and you have to choose which branch the pipeline cares about, with no right answer.
The deeper problem is rarely git flow as written, and this is the version we’d bet on seeing in your repository. Teams adopt it, don’t quite understand it, hit friction, work around the friction, and keep calling the result git flow. The official process and the real one have drifted apart, people blame each other for not merging to develop, and commits land on branches nobody can account for.
Two questions tell you which process you’re actually running, and neither needs tooling. How many branches does this repository have right now, and how many of them do you need? The number matters less than whether anyone knows it without going to look. Ask how many of them are work in progress and you find out whether the team can tell a live branch from one nobody has the heart to delete. Then ask how many repositories you have. The answer is usually either one or too many, and the right number is somewhere in between.
The remedy splits us. Johan would rather a team run git flow honestly for two months, by the book, and then ask what worked. Andrey wants the reasoning first: find the problem the practice was built for, then check whether it’s your problem. Adopt it only then, because the alternative is a fad moving around the internet. We agree on what doesn’t work: the three and a half day experiment that ends with “this sucks”.
Trunk based development, and the branch that earns its keep
Saying no to a model is easy. The follow-up question is fair, and it has a published answer.
If not git flow, then what?
Commit to trunk, branch only when a release needs it, fix on trunk, and let that branch die with the release.
Trunk based development spells out the discipline that makes a release branch safe. Cut it late, days before the release, while everyone keeps committing to trunk, and keep features off it. When a bug comes in, reproduce it on trunk and fix it there with a test. Cherry-pick the fix to the release branch and run CI there too. Passing tests on trunk don’t establish that the fix works against the older release code. The direction matters: “You should not fix bugs on the release branch in the expectation of cherry-picking them back to the trunk.” The fix you forget to carry home comes back as a regression weeks later. The page does allow the other direction when you “absolutely cannot reproduce the bug on trunk”, and prices it: “you have introduced risk of regression”. Shipping the patched release doesn’t close that. Write the test on the release branch: it has to fail before the fix and pass after. Then check trunk separately and port any correction it needs.
Two different branches get called a release branch, and conflating them is how this argument goes in circles. Common Flow, the unbranded version of this workflow, keeps them apart. A short-term release branch prepares one version, and it is optional. Common Flow calls it “RECOMMENDED if there is a lengthy pre-release verification process” that would otherwise freeze the mainline. Once the release is tagged it “MUST be merged back into its source branch and then deleted”. A long-term release branch carries a version that is no longer on the mainline, and gets “the same respect as the master branch”. Embedded companies live in that second case, obliged by contract to keep an old release compilable and patchable. A customer who certified version 4.2 is not going to take 5.0 because your workflow prefers it. That’s the only reason either of us could name for a branch that outlives its release, which isn’t to say there are no others.
The rest of Common Flow is “essentially GitHub Flow with the addition of versioned releases”: one main branch, short change branches, releases as tags. GitHub Flow is the same shape without the versioning: anything in master is deployable, and you deploy immediately after merging. GitLab publishes another variant. Same animal, different logos. Any of them is a decent place to start.
What a branching change bought one business
None of this is worth an argument for its own sake. The payoff shows up somewhere the people funding your team can see it.
What does a branching model have to do with the business?
It sets how quickly you can put something in front of the people who pay for it, and how much work they have to redo before they trust it.
In 2011 we worked on moving Ericsson from ClearCase to Git: an embedded organization, with real obligations to ship stable releases and support them afterwards. We ended up with something we called the cactus model. Everyone worked on the primary branch for as long as they could. Every two weeks we branched for a release and fixed what that release needed on the branch. We cherry-picked those fixes back to the primary branch, and let the branch die off after a couple of months of support. Alongside it went a large, sustained investment in continuous integration and test automation.
Note which way those fixes travelled, because it’s the opposite of the discipline above. We fixed on the release branch and carried the change home by hand, the habit the trunk based development page warns about. Starting from scratch, fix on trunk first and cherry-pick outward, unless the bug refuses to show up there.
Then the branches started to empty out. Fewer and fewer commits landed on each release branch, until we were cutting branches that received nothing at all. The teams receiving our deliveries noticed before we told them, and asked for nightly builds instead of two-weekly ones. Their operators were testing continuously in their own labs. By the time a release was declared, everything in it had been through the lab, and the release was a formality with an SLA on top. For Ericsson that collapsed the time between finishing a release and getting paid for it.
Source code and artifacts are different workflows
That payoff assumes you can say what is actually running. One question tells you how much of your confidence is real: is the code on the main branch what’s in production? If manual testers work through a UAT cycle, or a regulated change board meets on Thursdays, days pass between the merge and the deploy. During those days your repository describes a system that doesn’t exist yet. Git is the wrong place to close that gap.
Why can’t Git tell you what’s in production?
Because “it’s almost never our source code that we deploy into production”, and the traceability you keep pushing into branches belongs to the artifact instead.
The qualities that matter change at the build boundary. While you’re in the source code, the only one worth optimizing is how easy the code is to change. Once there’s an artifact, a different set applies: it’s immutable, it moves between environments, and it’s what you trace back when something breaks at three in the morning. We tie those two too tightly together, then expect Git to carry the whole truth, and it can’t.
Decoupling them is what makes asynchronous steps survivable. If a build has to run on physical hardware for a week before it’s qualified, that step can’t be a merge gate without stopping everybody. The source workflow keeps moving while the artifact workflow takes its time, and working out which change in an artifact broke it is a question for artifact management.
Code review is the gate worth re-examining first. Does it have to happen before the merge? “It’s not a merge criteria, it’s a release criteria” is a position we like more the longer we sit with it. None of us has worked out where it stops being true, so treat it as something to try rather than a rule. Immutability, containerization and real artifact management move the industry the right way, and the vocabulary for talking about the split is still thin.
What this means for teams
Your branching model isn’t a style preference, and it isn’t really about Git. It’s the mechanism that decides how often your team integrates, how quickly a mistake becomes visible, and how much rework sits between a finished change and a customer. Teams that get this right spend less time on branch archaeology and more on the product, and the ones that don’t keep paying in merge sprints they’ve stopped noticing.
Leaving git flow is a sequence, not a flag day. Finish or merge what is outstanding on develop first, because that is the integration branch and unreleased work lives there. Repoint CI, branch protection and release automation at the mainline while both branches still exist, and retire develop only then. Shrink feature branches to a day or two afterwards, as a separate change, and watch the active branch count as you go.
Four things you can do this week:
- Count the active branches in your busiest repository, and for each one, name the person who needs it and what it’s waiting for. Delete the ones that fail both tests. Then time your regression suite end to end: that number is your branching budget, and shortening it is what buys you the option to stop branching.
- Pick one repository running git flow and walk that sequence, until releases are tagged on the mainline. Cut a short-term release branch when a long verification would otherwise freeze the mainline. Keep a long-term one only for a version you have to support and can’t roll forward.
- Run one experiment on a slow gate. In a single repository, make code review or UAT a release criterion instead of a merge criterion for a month. Then compare branch lifetime and escaped defects against what you had before.
- Listen to your developers for a day and write down the words they use for the moves they make most: deliver, sync, park this for now. Give each of those words a
gitalias or a button in the IDE. Day to day we talkgit addandgit commit, an abstraction level much closer to the iron than to the problem anyone is solving. New joiners pay for that gap. Aliases in your team’s own vocabulary are the cheap way to make Git stop mattering.
Common questions, answered
How do we get off git flow without stalling releases?
Drain develop first, repoint CI and release automation at the mainline, and delete the branch only then. In the published model, develop is the integration branch, so unreleased work and anything keyed to that branch name needs a new home before it goes. After that a change has one merge of its own and nothing batched behind it. If you ship on a fixed cadence, cut a short-term release branch while you move: days before the release, features off it, bugs fixed on the mainline and cherry-picked across. Shrink feature branches as a separate step.
Do we still need a release branch if we deploy every day?
Probably not: release from a tag on the mainline and roll forward when something breaks. Two different things get that name, though. A short-term release branch prepares one version when a long pre-release verification would otherwise freeze the mainline, and it is merged back and deleted once the release is tagged. A long-term one carries a version that is no longer on the mainline. It earns its place when someone can’t take your latest build: embedded products with support contracts, software a customer certified. If neither case is yours, a release branch stores risk you could be shipping through.
Can we still review code if everyone merges to the mainline daily?
Yes, and the open question is whether the review has to block the merge. Treat review as a release criterion instead of a merge criterion, as an experiment rather than a policy. Pick one repository and a fixed period, then watch branch lifetime and the defects that reach production. We like the idea and can’t say where it stops being true, so test it where being wrong is cheap. What’s in its favour is throughput: a review queue in front of the merge is a branch-lifetime generator.
Related episodes
- Episode 6, SemVer or Not to SemVer is Johan Abildskov’s first visit, on versioning what you ship. Common Flow’s release tags only mean something if you’ve already settled what a version number promises.
- Episode 2, GitOps is the same source-versus-artifact divide seen from the deployment end: what an agent pulls from Git, and what it actually runs.
- Episode 16, Do You Need a Staging Environment? covers where verification happens before production, including why a branch environment isn’t a staging environment.
- Episode 14, Theory of Constraint is the framework behind the argument here: speed up anything other than the constraint and nothing moves. For most teams, branching is downstream of how slow verification is.
Resources
- A successful Git branching model, nvie settles what git flow actually specifies, and carries the author’s March 2020 note of reflection recommending a simpler workflow for teams delivering continuously.
- Trunk based development, branch for release settles the discipline that makes a release branch safe: cut it late, keep features off it, fix on trunk and cherry-pick outward. Let CI verify the branch too, and delete it when the release leaves production. It also covers the exception, a bug you cannot reproduce on trunk, and says the branch-first fix leaves you with risk of regression.
- Common Flow 1.0.0-rc.5 settles the unbranded middle ground: GitHub Flow plus versioned releases. It also settles the split between a short-term release branch, merged back and deleted after the tag, and a long-term one for versions off the mainline.
- GitHub Flow settles the simplest published variant: anything in
masteris deployable, branch off it, open a pull request, merge and deploy immediately. - Patterns for Managing Source Code Branches, Martin Fowler settles the difference between branching and integration, and why integration frequency is the variable that matters.
- Basic merging, Version Control with Subversion settles what a Subversion branch costs to create: a constant-time copy on the server. It also settles what weeks of isolation cost in a working copy that is never committed or updated. What it doesn’t measure is the cost of a long-lived branch.
- Trunk based development, DORA settles the measured thresholds from the 2016 and 2017 State of DevOps data: three or fewer active branches, merged at least daily. It doesn’t publish effect sizes.
- Practical Git, Johan Abildskov (Apress, November 2020) is our guest’s book, built around the mental model and hands-on exercises rather than command recipes. The Git Katas behind it are free to run with your team this afternoon, and Johan’s work is on LinkedIn.
Since we recorded
Meta open sourced the Git-compatible Sapling client in November 2022, which clones and works with existing Git repositories. GitHub merge queue went generally available in July 2023, and it tests each pull request against the latest base before letting it land. That is a mechanical answer to green on your branch meaning nothing. The argument held up, and we came back to it in episode 107, Continuous Integration in 2026.
