Why feature branches suck and cherry-picking even more.
In recent years I accompanied some companies in the transition from svn to git. And again I got involved in such an endeavor.
Currently using svn and trunk-based development. Many teams working on a shared codebase. There are issues with conflicting changes. With such problems in mind, feature branches sound like a reasonable solution — I don’t think so.
The new workflow should create an environment where teams can work in isolation until a feature is done to be merged into the master branch. From there the software can be released with ease.
With previous clients, I had experienced similar transitions. At first, we moved to git. Then we introduced feature branches. But issues came up we could not solve. Finally when the process became agile the natural step was to move to trunk-based development. It was scary at first. But solved a lot of issues.
Late integration sucks
Feature branches work best when you avoid changing existing code.
But code is not like a building made of concrete. It can evolve, and should. With evolving code and long-living feature branches, you will eventually get conflicts that are hard to solve.
Imagine several teams working on different features, but sharing some code. Someone wants to refactor a part of the code. The signature of a method gets changed. The change gets committed to their feature branch. Days or weeks pass until the work on the feature is done. The whole branch merged back to the mainline. Probably without a conflict. All good so far.
The changed signature is of a core method. The other teams have their feature branches. And added code using the former definition of the method. The next day they pull the mainline into their feature branch. Which results in conflicts.
Especially when changing core components, one change in one feature branch can yield conflicts in many branches. The receiving teams now need to fix these conflicts.
The cause is deferred integration. Integration does not happen with each commit, but only after a feature branch is finished and merged back into mainline.
In contrast with trunk-based development, breaking changes raise a conflict immediately. And it gets fixed by one for all immediately.
Trunk based development allows for continuous integration. Every commit should be a small but working and complete addition to the software. With feature branches, a finished state is only required at the end. This allows developers to commit uncompleted work to a branch.
Read more about how CI is more than just a server in Less Works: Continuous Integration.
Breaking up a monolith
With the late integration of feature branches, it gets harder to refactor a system. This is slowing you down in the effort to break down a monolith into smaller pieces. Breaking down a monolith requires a lot of refactoring. Classes get moved to different packages and methods to different classes to form the modules that are going to be the small services.
These changes can cause huge conflicts that need to be fixed within each branch later. It is possible, but a nightmare. Cleaning up a mess is hard, but it gets harder with feature branches.
Cherry-picking is an emergency solution
With feature branches, you only get to a deployable state of your feature only when the branch is merged back into the mainline. That means you can only release your feature as a whole when it is done.
But could we maybe release just a part of that feature to the test server and later into production. Can we have part of the branch? Cherry-picking allows just that. Find the commit you like to put in a release. Sounds like a solution.
Cherry-pick with caution! You can include dependent commits without even noticing. I have seen it a couple of times already. Never has anybody realized that behavior immediately. Our stakeholders wondered why suddenly parts were available on the test environment that should not be there by now. Developers spent hours to figure out what happened by analyzing the git history.
There is a nasty downside of cherry-picking in git. Based on the root of two branches, If the cherry-picked commit depends on another commit which affects the same line range in code, this commit also gets included. Otherwise, git cannot properly apply the diff to the file. So cherry-picking does not guarantee to only pick the selected commit. It takes what it needs. And that can be a lot.
Therefore cherry-picking can only be an emergency solution used carefully.
Different branches have different changes in the database. After merging back to the mainline it can be required to apply the combined schema changes in a specific sequence. You will only find out when merging to the mainline. Don’t forget to check. Git won’t tell.
Also, the deployment of different feature branches to the same test environment is a challenge to overcome. You cannot guarantee that database evolutions of different branches stay compatible.
Choose your workflow wisely
In my experience feature branches can be quite a pain.
As shown above, it is hard to practice continuous integration. Trying to do so creates more issues that need to be taken care of.
New code is good. Changing existing code can include high effort to integrate into all feature branches. Do you want to move to microservices? Try that without refactoring.
Although cherry-picking works most of the time. You will eventually create a mess by accident. Don’t make it a major mechanism in your workflow.
Think about the coordination and overhead needed to have different feature branches on a test environment. Database evolutions will break. And someone needs to fix it.
Just stay on the mainline.