One of the things that has always fascinated me about software engineering is how quickly we forget the problems that older solutions were designed to solve. GitFlow is a good example.
If you spend any time reading modern engineering blogs, you'll eventually run into someone explaining why trunk-based development is superior and why GitFlow is outdated. The conclusion may be right, but the explanation is often incomplete, because what usually gets lost is the context. GitFlow didn't become popular because engineers were foolish. It became popular because it solved real problems that many organizations were facing at the time. To understand why so many teams are moving away from it today, it helps to understand why it made sense in the first place.
GitFlow Solved Real Problems
I started writing software before source control was something every team took for granted. If you wanted to know who changed a file, you often had to ask around, and if two developers modified the same piece of code, integrating those changes could become an adventure. When centralized source control systems became common, they felt revolutionary, because they solved problems that had previously been handled through discipline, communication, and a fair amount of luck.
Then Git arrived and made branching inexpensive. Creating a branch became so easy that most developers stopped thinking about it. If you needed a branch you created one, and if you needed ten branches you created ten. Branching went from being a significant decision to something that happened almost automatically, and that was a genuine improvement.
Around the same time Git was becoming dominant, GitFlow emerged as a structured way of organizing work, and looking back, it made a tremendous amount of sense. Most organizations were not deploying continuously. Many teams released monthly, and some released quarterly. Automated testing existed, but it wasn't nearly as comprehensive or reliable as what many teams have today, infrastructure automation was still maturing, and deployment weekends and release freezes were common. Getting software into production carried significant risk.
In that environment, GitFlow provided structure. Feature branches let developers work independently, release branches created stability while final testing occurred, and hotfix branches provided a mechanism for urgent production fixes. The model helped teams coordinate delivery in a world where deployments were expensive and often stressful, and there is nothing irrational about any of that.
Cheap Branches, Expensive Integration
The catch is that Git made branch creation cheap, but it didn't make integration cheap, and those are two very different things.
This distinction matters because software engineering has a long history of confusing local optimizations with system optimizations. Creating a branch is a local optimization that makes life easier for the developer doing the work. Integrating that branch back into the system is a system problem, and the fact that one became easier never changed the complexity of the other.
While many organizations were refining increasingly sophisticated branching strategies, the industry was also getting dramatically better at automation. Build systems matured, automated testing became more reliable, and cloud infrastructure reduced the operational friction associated with deployments. Continuous integration and continuous delivery moved from niche practices to mainstream engineering capabilities. At first glance those developments might not seem related to branching strategies, but I think they are deeply connected.
CI/CD Changed the Economics
The reason is that CI/CD changed the economics of software delivery. When deployments are difficult, expensive, and risky, it makes sense to optimize for release management. When deployments become routine, reliable, and largely automated, the bottleneck shifts. The challenge is no longer moving software into production, it is integrating changes safely and continuously, and that shift sounds subtle but it changes almost everything.
Many discussions about trunk-based development focus on branches, and I think that misses the more important point. The real issue is not branching, it is integration delay. Every day that code remains isolated on a branch is another day that the system has not learned something important. The developer may have learned, the branch may appear healthy, and the feature may even seem complete, but the broader system still doesn't know whether that work integrates cleanly with everything else that is happening.
Uncertainty Accumulates Quietly
That uncertainty accumulates quietly. Another team modifies a dependency, a shared library changes its behavior, an assumption turns out to be incorrect, or a feature that worked perfectly in isolation behaves differently once it is combined with other changes. None of these problems are unusual. In fact they are entirely normal, because software systems are constantly evolving, and the longer integration is delayed, the more opportunity there is for divergence to occur.
This is one of the reasons I have come to view long-running branches as a tax. Not because branches are inherently bad, and not because every branch immediately creates problems, but because the cost of a branch grows over time, and the people paying that cost are often not the same people receiving the benefit.
Who Pays for a Long-Running Branch
The developer creating the branch gets isolation, and that isolation can be valuable, because it allows focused work without constantly dealing with changes from other developers. The system, however, inherits a different set of problems. Testing becomes more complicated because there are now multiple versions of reality. Release planning becomes more complicated because someone has to decide when and how those realities come back together. Operational risk increases because integration is happening later in the process, when more assumptions have accumulated and more stakeholders are involved.
The merge conflict itself is rarely the real problem. Engineers sometimes talk about merge conflicts as if they are the primary cost of long-running branches, but in my experience they are usually just a symptom. The larger issue is discovering that multiple streams of work have evolved independently and nobody knows exactly how they will behave once they meet, and that discovery almost always happens at an inconvenient time. The closer you are to a release, the more expensive that uncertainty becomes.
Trunk-Based Development Is a Consequence, Not a Goal
This is where trunk-based development starts to make sense, and not because it is fashionable, or because large technology companies use it, or because someone declared it a best practice. It makes sense because it aligns with the same forces that made CI/CD valuable in the first place. If the purpose of automation is to create repeatability, then having a single integration point simplifies automation. If the purpose of automation is to reduce uncertainty, then integrating continuously reduces uncertainty. If the purpose of automation is to make deployments boring, then shortening the distance between writing code and integrating it helps make that possible.
One of the questions I often ask is this: if our goal is to automate software delivery, why would we intentionally choose workflows that create multiple competing versions of reality? That is not a criticism of GitFlow, it is simply a recognition that the surrounding environment has changed. The more mature an organization's automation becomes, the more pressure there is to integrate earlier, and the more frequently an organization deploys, the more expensive integration delays become. Eventually you reach a point where optimizing for continuous integration matters more than optimizing for long-term isolation, and at that point trunk-based development starts to emerge naturally.
What I find interesting is that many discussions present trunk-based development as if it were the goal. I don't think it is. I think it is the consequence. If you care about rapid feedback, lower operational risk, reliable automation, and boring production, frequent integration becomes important in every one of those cases, and once you follow those ideas to their logical conclusion, trunk-based development is often where you end up.
You Choose Where to Pay
Over the years I have become increasingly convinced that most engineering decisions should be evaluated through the lens of the entire system rather than the convenience of any individual step. A branch may make one developer's work easier today, but that does not automatically mean it makes the system healthier tomorrow. The system is what happens after the code ships. That is where operational costs appear, where integration costs appear, and where uncertainty eventually reveals itself.
Long-running branches are not dangerous because they violate some engineering rule. They are dangerous because they allow uncertainty to accumulate in places where it is difficult to see and expensive to resolve. Trunk-based development does not eliminate risk, because nothing does. What it does is force the system to confront uncertainty earlier, while the problems are still small, the context is still fresh, and the cost of fixing mistakes remains low. In my experience, that is one of the few tradeoffs in software engineering that continues to pay dividends year after year.
You cannot eliminate complexity from software delivery. You can only choose where you want to pay for it. Long-running branches delay the bill, continuous integration pays it immediately, and most of the time, paying immediately is cheaper.
Frequently asked questions
Why are long-running branches considered a tax?
- Because the cost of a branch grows the longer it stays open, and the people paying it usually aren't the ones getting the benefit. The developer gets isolation, while the system inherits harder testing, more complicated release planning, and integration risk that lands later, when more assumptions have piled up.
Was GitFlow a mistake?
- No. GitFlow solved real problems in a world where teams released monthly or quarterly, automated testing was thin, and deployments were risky. Feature, release, and hotfix branches gave teams structure when getting software into production was expensive and stressful. What changed is the surrounding environment, not the reasoning behind it.
Isn't the real cost of a long-running branch the merge conflict?
- The merge conflict is usually a symptom, not the real cost. The deeper problem is discovering that several streams of work evolved independently and nobody knows how they behave once combined. That discovery tends to happen close to a release, which is exactly when uncertainty is most expensive.
How did CI/CD change branching?
- CI/CD changed the economics of delivery. When deployments are hard and risky, it makes sense to optimize for release management. When deployments become routine and automated, the bottleneck shifts to integrating changes safely and continuously, which rewards frequent integration over long isolation.
Is trunk-based development the goal?
- Not really. It is the consequence. If you care about rapid feedback, lower operational risk, reliable automation, and boring production, you end up integrating frequently, and trunk-based development is usually where that path leads.
Does trunk-based development eliminate risk?
- No, nothing does. What it does is force the system to confront uncertainty earlier, while the problems are still small, the context is still fresh, and the cost of fixing mistakes is still low. You cannot eliminate complexity from delivery, you can only choose where to pay for it.