There are many branching models. In most cases their intention is clear – to separate development into several streams. Commonly mentioned benefits include faster implementation of new features due to a lack of interference with other changes, increased stability of particular branches, controlled work publishing to a baseline, easier identification of which changes broke which particular test or area of application.
Unfortunately, branches that require them to be merged at some point in time introduce a major problem – their integration to a baseline. You have probably participated in a horror merge more than once. I was personally participating in a merge that took more than 2 months to complete (and there were several people involved). Moreover, many such stories end in a huge amount of regression in a trunk/master which results in another days, weeks or even months of bugs fixing (that we, of course, very much enjoy!).
This approach doesn’t also go in pair with Continuous Integration practice. We would like to always have a stable and releasable baseline which integrates daily all changes from the different development streams. It is also a prerequisite for introducing Continuous Delivery. Unfortunately, it means that all developed code should be committed on a daily basis, whether the new feature is fully implemented or not. To achieve it, we can follow one of four ways: hide new functionality until it is ready, perform incremental changes, use branch by abstraction pattern or componentize your application.
Lets start with the most obvious path – perform incremental changes. While this way is not always applicable it is possible to follow it with some additional effort in most of the cases. It often requires additional analysis and a little more code that can be cleaned afterwards. Coding this way often leads to dividing requirements into smaller tasks and results in a better understanding of the topic by the developers. However, as mentioned above, there are changes that cannot be handled this way.
Another way of dealing with incremental changes is to hide new functionality until it is finished. The main idea is that features can be turned on and off by a property or some kind of license mechanism. There is some architecture impact and additional complexity to the system but gains are significant – all already written code for all unfinished features is integrated with a baseline. Moreover, sometimes blocking a new feature can be as simple as blocking access to a URL under which new functionality will be available.
The third way, branch by abstraction, is a little bit more complicated. It is divided into two major steps:
- refactor the system in a way that current and new functionality will be plugable. As a result, you should end up with an abstraction layer over the current code and current code using this abstraction layer,
- write new functionality in a way that it will be able to replace the current one without changing the abstraction layer.
Then, either programmatically or by some configuration you will be able to easily switch old implementation to a new one. This way is the most complicated out of the three presented up to now and might require a lot of additional effort. On the other hand, such refactoring usually improves design significantly. Moreover, the abstraction layer can be removed afterwards if there are strong assumptions that it will not be required in the future or overcomplicates a system.
The next way to deal with branches is to componentize your system. The topic is so broad that dozens of books have been written about it already. To just scratch the surface, the components allows you to decouple parts of your application which are highly independent on each other, have different lifecycles or change at different rates. Unfortunately, managing dependencies between components sometimes requires a significant amount of effort. Moreover, all Continuous Integration and Continuous Delivery processes become more complicated and potentially more fragile.
Everyday integration of all changes related to your system provides tremendous value. Without it, even if you think that you are using Continuous Integration methodology, you lose most of the benefits that it provides. Properly implemented it allows you to develop pretty fluently and without hitches. Feature or refactoring branches looks promising. They are very tempting and many organizations are not able to resist. However, the whole gain of uninterrupted development is usually lost during integration with the baseline phase. The more simultaneous development happens the bigger the problem is during a merge. Thus it is worth it to avoid branches as much as possible. With the 4 above strategies we can do in almost all cases.