There’s quite a few different approaches to how you go about working with version control in a team. There are some common patterns like gitflow or feature branching from master. It’s also not uncommon for teams to come up with their own strategies with varying complexity. I’m going to discuss in this post how you should focus on extreme simplicity with your branching strategy and why approaches like gitflow seem to go against modern engineering principles.
Let’s start by talking about what we value
I’d expect most of us agree that the below points are all fundamentals of modern engineering:
- Code should be continuously integrated
- Integrated code should be continuously tested
- Integrated code should be regularly shipped to production
- Code should be created via collaboration (pairing or design and review sessions)
There's a great post on modern extreme programming by Benji Webber that talks about continuous deployment in these terms
- Deploy to production after no more than a couple of hours
- Not just build but deploy to production in under ten minutes
- Allow the business to choose to release whenever they wish because we decouple deployment from release
That seems like a sensible target state, let's talk about how branching can hold us back from achieving that.
Some problems with branching
At it’s core most branching strategies suffer from one key issue - it prevents code being regularly integrated. Branching often reduces the cadence with which your entire team bring the entire code base together. That’s not to say all branching is necessarily a bad choice, it’s just a downside we should be aware of.
Most branching strategies tend to consist of the notion of feature branching in some form. The idea of feature branches is to create a safe environment for an engineer to complete and test a feature in isolation. Code review can then happen on a complete feature and when ready merged into a release branch or master.
Unfortunately this one to one mapping of features to branch causes a number of issues. Some of these might not seem bad with small features but for larger features (and features have a nasty habit of sometimes being larger than you first thought) these can become real problems.
We’re likely to spend days in isolation from the rest of the code base, not frequently testing our changes with changes that are happening elsewhere within the team.
Code reviewing a large feature can be pretty laborious stuff and also a pretty terrible time in the process to disagree with design choices. If you’re pairing or doing regular reviews anyway that’s great but feature branching does tend to encourage a single review.
Incremental release roll out
Once you’ve merged your feature into whatever branch you use for releasing it’s a bit of a challenge to incrementally deploy your change. It is possible to use a load balancer to divert a certain percentage of traffic to your new deployment but anything more targeted than that isn’t possible.
The queuing problem
Once you’ve merged a feature branch in and you’re testing it on your UAT environment it’s not easy to prioritise another change (like a hot fix) into production without either reverting your original feature change set or simply releasing both changes at once. Gitflow introduces dedicated release branches to combat this issue but it adds more complexity.
Focus on your pipeline
The slower your pipeline the less you integrate. Integrating regularly forces focus on a healthy, deterministic pipeline.
We spend all this time coming up with branching strategies, training the team on them, implementing them and managing merge conflicts all so that we can get half-complete features off our local machines and put it somewhere safer. So is there another option?
Feature toggles, branch by abstraction and UI last to the rescue
We have to ask the question of what the alternative is. How could we achieve some of the positive outcomes of feature branching without the downsides I’ve discussed above. Well there’s a number of in code strategies for preventing partially complete changes affecting your customers. Because these strategies are in code and not in version control it’s possible for us to bring our teams code together as frequently as we like, test regularly and release at any point.
Feature toggles are checks in code against configuration to determine if a certain path should be executed. Companies like Launch Darkly provide a managed service to make feature toggles easy to implement and manage and make incrementally rolling out changes to certain parts of your customer base easy. It’s also possible to link feature toggles with an A/B testing platform and automatic roll backs to make sure that your new feature works as expected and is producing the outcomes you’re after.
You can learn more about feature toggles from this Martin Fowler post
Branch by abstraction
Branching by abstraction is about building a layer of abstraction on top of a module or library you're looking to upgrade. You gradually migrate clients of the old module to this new abstraction layer which calls through to your existing module. Once you've migrated your clients you're able to switch out the underlying implementation to a new implementation of your module.
Get a better explanation of branching by abstraction including diagrams here
Large changes to an endpoint in your services can be implemented by rolling out a new endpoint instead of editing an existing endpoint. Similarly in the front end large changes can be done by creating a new page instead of modifying an existing one. If you tie these both together large full stack changes can be implemented via a hidden page on your front end, calling new endpoints. This makes testing really simple and ensures your codebase continues to work with large sweeping changes. Simply switch out your entry point or change something in your router to deploy your new version when you're ready.
I actually think branches can be useful as code review gates, although teams that pair program and exhibit a lot of discipline with test pipelines may feel like they don't need them at all. Moving away from branching and towards some of these in code strategies does carry some complexity and requires a certain level of team maturity. I've personally found these in code strategies useful to reach full continuous deployment and release many times per day.