Trisha summarizes the advantages of trunk-based development (as opposed to branch-based development) in this article.
These days, distributed version control systems like Git have "won the war" of version control. One of the arguments I used to hear when DVCSs were gaining traction was around how easy it is to branch and merge with a VCS like Git. However, I'm a big fan of Trunk-Based Development (TBD), and I want to tell you why.
With trunk-based development, all developers work on a single branch (e.g. 'main'). You might have read or heard Martin Fowler or Dave Farley talking about it. It's when I was working with Dave (around about the time that Git was rapidly becoming the "go to" version control system) that I really saw the benefits of trunk-based development for the team, particularly in an environment that was pioneering Continuous Delivery - Dave was writing the book with Jez Humble while I worked with him.
In contrast, the branching model encourages developers to create separate branches for every feature, bug fix, or enhancement. Although branching may seem like a logical approach to isolate changes and reduce risk, several factors make me more comfortable with trunk-based development.
1. Speed and Efficiency
In trunk-based development, the entire team works on a single branch. This model allows for quicker integrations and fewer merge conflicts. This is literally "Continuous Integration (CI)", as originally suggested by the practices of Extreme Programming. While these days we tend to mean "run your build and tests on a team server every time you commit" when we say CI, what CI really meant was actually integrate your code regularly. Code living on separate branches is, by definition, not integrated. And the longer these branches live for, the more challenging it is to merge them back into the main codebase. It might seem fast to develop your fixes and improvements on a separate branch that isn't impacted by other developers' changes, but you still have to pay that cost at some point. Integrating small changes regularly into your code is usually less painful than a big merge at the end of a longer period of time.
2. Greater Code Stability
Trunk-based development encourages frequent commits, which leads to smaller and manageable changes. How and why? For the same reason that we don't want big merges from long-lived branches - the longer we leave it to commit our changes, the higher the chances our commit will clash with someone else's changes. By frequently pulling in the other developers' changes, and frequently pushing small changes of working code, we know the codebase is stable and working. Of course, this assumption of "stable and working" is easier to check if we have a CI server that's running the build and tests for each of these commits. We also have to stop making commits if the build breaks at any time, and focus on fixing that build. Continuously pushing small, frequent commits when the build is already broken isn't going to do anyone any favours.
In the branching model, large and infrequent merges can introduce bugs that are hard to identify and resolve due to the sheer size of the changes. Have you ever merged the trunk into your branch after someone else has merged their own big piece of work and found your code no longer works? It can take a LOT of time to track down why your tests are failing or the application isn't working the way you expect when you've made a whole bunch of changes and someone else has made a whole bunch of different, or overlapping, changes. And that's assuming you actually have reliable test coverage that can tell you there's a problem.
3. Enhanced Team Collaboration
My favourite way of sharing knowledge between team members is pair programming. I know not everyone is a fan, or is in a position to do it (especially now more people are working remotely, but if so, check out JetBrains' Code With Me). If you're not pairing, then at least you want to be working on the same code, right? If you're all working on your own branches, you are not collaborating. You are competing. To see who can get their code in fastest. To avoid being stomped on by someone else's code changes.
If you're all working on the same branch, you tend to have a better awareness of the changes being made. This approach fosters greater team collaboration and knowledge sharing. In contrast, branching can create a siloed work environment where you're all working independently, leading to knowledge gaps within the team.
4. Improved Continuous Integration and Delivery (CI/CD) Practices
Dave Farley's book, "Continuous Delivery", and his log posts and videos, argue something along the lines of "trunk-based development is inherently compatible with Continuous Integration and Continuous Delivery (CI/CD) practices".
In a trunk-based model, continuous integration becomes more straightforward because your code is committed frequently to trunk, and that's the branch your CI environment is running the build and tests on. Any failures there are seen and addressed promptly, reducing the risk of nasty failures. It's usually easy to track down which changes caused the problem. If the issue can't be fixed immediately, you can back the specific changes that caused it.
By now we should know the importance of a quick feedback loop - when you find problems faster you can locate the cause faster, and you can fix it faster. This improves your software's quality.
Continuous delivery also thrives in a trunk-based development environment. Successful continuous delivery hinges on the ability to have a codebase that is always in a deployable state. The trunk-based development approach ensures this by promoting frequent commits, frequent integrations, and tests on all of these integrations. The small number of changes being introduced at any one time makes the software easier to deploy and test.
In contrast, implementing effective CI/CD can be more complex and time-consuming with the branching model. While it's tempting to think "well I run my build and all my tests on my branch", you're not actually integrating every time you commit. It's at merge (or rebase) time that you start to see any integration issues. All those tests you were running on your branch "in CI" were not testing any kind of integration at all.
Merging and testing code from different branches can introduce delays and potential errors, which takes away some of the benefits of having a build pipeline in the first place.
5. Reduced Technical Debt
Long-lived branches often lead to 'merge hell', where the differences between one branch (like 'main') and another (for example your feature branch) are so great that merging becomes a nightmare. This can result in technical debt as you may resort to quick fixes to resolve merge conflicts, or accept suggestions from your IDE that resolve the merge but that you don't fully understand. With trunk-based development, frequent merges and smaller changes make it easier to manage and reduce the build-up of technical debt.
In conclusion
I, personally, think trunk-based development has clear advantages, and I have experienced them first-hand working in teams that have adopted this approach. However, it requires a mindset, a culture, within the development team. You need to frequently merge in other developers' changes into your own code. You need to commit small changes, frequently, which requires you to only change small sections of the code and make incremental changes, something which can be a difficult habit to get used to. Pair programming, comprehensive automated testing, and maybe code reviews are key practices to helping all the team to adopt the same approach and culture.
Trunk-based development, done in a disciplined way, streamlines the development process, enhances team collaboration, improves code stability, supports efficient CI/CD practices, and may result in less technical debt. While it may be challenging to adapt to this approach if you've been working with a branch-based model, the long-term benefits are worthwhile. If you want to move to working this way, you may also want to read Dave's article addressing barriers to trunk-based development.
Most of the issues come from long lived isolated branches, so prefer a middle ground in micro branches. Branches are only allowed the live for days before they must be merged and a new branch started. This ensures frequent merged and development with the latest changes for others, while also allowing all the structured goodness of fully integrated test before merging to main for a stable main branch.
The problem I see with this is, how do you avoid accidently breaking the code for *everyone* with one erroneous commit? With a branching model (and appropriate division of labor) you won’t risk stomping on the whole team’s toes every time you commit code.
You made a lot of good points.
However, I do believe this is a bit of a straw-man of branching models:
“And the longer these branches live for, the more challenging it is to merge them back into the main codebase. It might seem fast to develop your fixes and improvements on a separate branch that isn’t impacted by other developers’ changes, but you still have to pay that cost at some point. Integrating small changes regularly into your code is usually less painful…”
Or maybe it’s more is a false dichotomy.
Good habits with branching can lead to the same results. GitHub PRs even encourage updating your branch from the trunk regularly, and lets you know when there are conflicts so that you can resolve them immediately.
This comes down to good practice and habits.
Even with trunk based development, you’ve still got to pull regularly, and sometimes stash and resolve conflict. This is one area that isn’t a real difference.
Anyone coding for a week without pulling, merging or updating has a teamwork problem, and that’s a team or management issue.
I don’t know, I feel that these tools and practices can lead to similar results, and this argument feels outdated – like it was written before GitHub, GitLab, etc…
Pipelines or commit hooks can test builds on feature branches too – so I can know that it all works as I commit to my branch, and before I merge to the trunk.
I’ll also echo the other Martin’s comment – how does one do code review?
Good point about competing and racing to commit first.
I develop for fun by myself, but I’m not quite sure how I would handle breaking changes that require less of changes to not break.
Trunk based works well for small teams that all work together on one feature. But it does not scale. The biggest problem I’ve found with it is the heroics required when a breaking cheng went in and now the whole team can’t work. There are a number of reasons git has been winning out, mainly the derisking and code isolation as you mentioned, if used properly it can encourage better review practices and team collaboration. Enevitably you will need a longer branch to do experiments, as your team grows I would encourage you to reevaluate your position.
I’ve used both, and in my experience branch based givrs much better stability of trunk. Divergance of branches can be avoided by developers taking responsibility to rebate often.
Everything else, small commutes, etc, good collaboration, can be done in a branch model
Pair programming and reviews and no PR?
No.
You negate so many other issues and things that must be accounted for. Migrations, infrastructure Improvements, a change in feature priorities. This does not encourage smaller code commits. Why do you think that? Because the developers we’ll be scared to push code? This doesn’t work in the real world with teams greater than 2 people.
I’m curious how would you handle code reverting due to changing customer/business priorities.
Let’s say the majority of a larger feature is already integrated into trunk, then the customer decides that that feature is needed in a very different way, or not at all. It seems to me that in these particular cases it can create tech debt overhead (or even unused code) and a challenge to revert many already integrated commits, instead of simply not merging a feature branch to trunk/master.
Code reviews are often misunderstood. We think they’re there to improve the quality of the code, and yet so few developers actually know what they’re supposed to be looking for in a code review that I would argue they often just get in the way of deploying working features. I have written about anti-patterns in code reviews (and what to do about them) https://blogs.oracle.com/javamagazine/post/five-code-review-antipatterns and I have also written about how to use different styles of code reviews depending on your goals, which includes some that work for trunk-based development: https://www.jetbrains.com/help/upsource/code-review-practices.html#process
In terms of the practices that need to be applied to make trunk based development work, feature toggles are an important thing, as is the ability for ANYONE to revert a commit that breaks the build, regardless of whether they worked on that code, in order for the team to continue working. The team cannot down tools and stop working because someone broke the build. The commit needs to be rolled back/reverted and then the developer(s) needs to fix it before recommitting.
Git allows for a trunk-like development (Gerrit is a great example).
Trunk based tools do not allow easy branching.
It’s not about trunk or distributed. It’s about Git being a much more comfortable tool to work with.
How would you reconcile code reviews (which usually happen on a pull request) with trunk based développement?