Use of a source control system (e.g. Git) starts with the relatively simple idea of branching from, and committing back to, a shared codebase. But once you start working with branches, exactly how to leverage the flexibility of these systems can quickly become daunting as you consider the (sometimes competing) goals you might have as an organization: Do I need to support a team of 20 engineers? Can we make changes to our production software whenever we want? Do we need to support several versions of a codebase simultaneously?
The answer? You need a branching strategy. Fortunately, developers and teams have been dealing with these questions for almost as long as source control systems have been around, and there’s a wealth of ideas, experiments and experience from which to draw some conclusions.
A branching strategy (or “development workflow”) is essentially a set of rules that a team of developers can follow to govern how they all interact with a shared codebase. An effective and efficient development workflow should accomplish the following:
In short, having a good development workflow will improve developer happiness, while helping your team ship code safely and efficiently.
Of course, your strategy might need to grow and adapt as the makeup of your team changes, as what makes for a great development workflow for one group might not be ideal for another.
When evaluating a branching strategy for your team, there are several factors that need to be considered.
While more contributors can often lead to a more sophisticated and capable product (and/or one that is developed more quickly), it also increases the likelihood of conflicts or other issues in the code. Your workflow process should be robust enough to handle the volume of work your team produces.
Leveraging continuous integration tools within your workflow can protect the team against unexpected integration errors that tend to slow down development. With this greater margin for error, you may be able to consider more sophisticated branching strategies with increased confidence.
What is the extent of your test coverage? What is the general culture on your team around testing? When new and existing code is covered with unit tests, it reduces the likelihood of introducing regression errors into the main branch.
Even a little automation can go a long way towards reducing friction in your workflows. For example, you could have a script that automatically cherry-picks certain commits from your main branch to a release branch based on a predefined naming pattern or keywords.
The first modern version control systems appeared almost 20 years ago, and in the interim we’ve seen plenty of different approaches to team workflows. However, many of them aren’t ideal for modern mobile software development.
When building mobile apps, as with any binary-deployed software (and unlike web-based applications), “roll-backs” or fixes often can’t be seamlessly or quickly deployed — so it’s particularly important to protect production-ready code from unwanted regressions during releases. (Read more about the challenges of mobile releases in our previous post.)
Below, we’ll consider two particular strategies that have proven to work well for mobile development.
GitFlow uses a tagged master branch, a development branch, versioned release branches, feature branches, and hotfix branches.
Work-in-progress code is contained within feature branches that are created off of, and merged back into, the main integration “development” branch. Code getting deployed to production is built from short-lived release branches that are created off of the development branch, then merged into master when the release is ready (so master always contains the latest production code), and then finally deleted.
Tags mark a snapshot of exactly what code was pushed live for every released version, and these reference points can be used to inspect the source code of older versions that are still out in the wild.
Referring back to our critical features of an efficient and effective strategy:
GitFlow’s use of release branches ensures that production-ready code will be protected from possibly unstable changes from continuing development on future releases. In addition, any code that has already made its way to production is protected and isolated in the main branch. This is an approach that is well-suited for explicitly versioned software, and works well when it becomes necessary to support multiple versions of live software.
GitFlow’s structure works well for larger teams because it allows for a dedicated development branch that can handle relatively unstable changes. When a release is cut, development can continue normally on the development branch, while code being prepared for production is “quarantined” on a release branch until it’s stable and ready for deployment. Finally, your main branch is reserved only for mature and stable code that was deemed worthy of release (and the main branch’s head will always represent the most current version in production).
In most situations and for most developers, GitFlow has relatively low overhead. When working on a new feature, developers will create a feature branch off the development branch, and merge it back into the development branch upon completion.
However, there is a bit more overhead when preparing for a release. Teams need to create the release branch, then make sure that any final work going into the release is also merged back into the development branch, and finally that the release branch is merged into the main branch when ready. Often, it will make sense to try to leverage some degree of automation to simplify these steps during the release process.
With trunk-based development, each contributor merges small commits directly into the master branch (or ‘trunk’ — think of the widest part of a tree and how it dwarfs any branches in importance and length). This approach only works reliably when there is a robust CI system in place; as builds are run and must pass before the corresponding code is merged back into the master branch.
Release branches are cut from the trunk on a just-in-time basis. Bug fixes can still be done on these release branches, and are then cherry-picked back into master.
Trunk-based development can theoretically work well at scale; Google and Facebook have made use of this approach to allow hundreds or thousands of contributors across multiple time zones to work with new code as quickly as possible (and lessen the likelihood of commensurately awkward merge conflicts from stale branches). But, as you’d expect, these are also organizations that have been able to invest heavily in automation and testing to safeguard workflows at these volumes.
While there is lots of activity happening on the trunk (which could correlate to relatively less stability), using release branches still serves to isolate production-ready code (similarly to GitFlow). In addition, there’s usually a higher bar for the general quality of code in a trunk-based workflow, because there’s only one working branch.
During development, your workflow is extremely straightforward — no branches are created; instead small commits are made and merged directly into master if builds are run successfully.
For “scaled trunk-based” teams, the experience will be similar to GitFlow for an individual contributor during development, as they will create a feature branch off of master, and merge back into it after builds are run successfully.
During releases, the mental overhead is reduced due to the lack of a “production” branch to manage separately. Any final fixes — or even a hotfix — for a release would ideally be created on master and cherry-picked to the release branch (eliminating risk of regression on the trunk, which is a core principle of this type of workflow). Friction here could, again, be lessened by automation.
Trunk-based strategies can be used without any release branches at all, by directly tagging commits on the main branch. Any necessary hotfixes can be cherry-picked into branches created from the tagged commit.
This further reduces mental overhead by removing the need to maintain release branches, but puts an even greater emphasis on having a master branch that is extremely stable or “release-ready”. This is certainly possible with robust testing and integration practices.
OneFlow - A branching strategy that (via liberal use of rebasing) attempts to preserve GitFlow’s advantages but improve upon a couple of specific weaknesses — by using a simpler conceptual model and preserving an understandable Git project history on your main branch.
Enhanced Git Flow - This method also attempts to improve upon perceived drawbacks of classic Gitflow — the overhead, unreadable Git history, and double merging releases — by force pushing a new main branch during every release.
ThreeFlow - A low-overhead approach that maintains three stable, long-lived branches (main, candidate, release) representing internal, beta, and production builds, and simple rules for what types of code get pushed to which.
GitHub Flow - An extremely simplified approach that’s more optimized for continuously deployed software (such as web applications), but can be a reasonable choice for small, early-stage mobile development teams that need to iterate and move quickly in non-production environments.
GitLab Flow - Building off of the principles of GitHub Flow above, this strategy adapts multiple environment branches and is additionally optimized to integrate with issue tracking systems.
How much overhead your branching strategy has essentially comes down to how many branches need to be maintained manually. More branches generally means more overhead, unless your team invests in automation.
Moving the team towards fewer branches and a tighter development workflow (short-lived feature branches merged within a couple of days to master) can certainly reduce cognitive load, but it’s a strategy that will only work well with a stable codebase — and that means a team that is willing to buy into a rigorous testing and integration culture.
For some teams, a relatively more complex strategy (and accompanying overhead) is warranted if the team prefers additional safety measures that protect the quality of production code. Having a “production” branch could be worth it if your team is anticipating severely unstable changes on the development branch; and if something goes horribly wrong there, you always have your “clean” production branch as a fallback.
Ultimately, choosing the right branching strategy for your team comes down to answering some questions honestly about your team and your goals — and being prepared and willing to make the necessary changes to improve your workflow.