Improve Your Release Process With Trunk-Based Development
As software developers, our main aim is to improve our businesses by releasing great content to our customers. Releasing and evolving that content creates real positive business impact.
I work within a group of developers trying to achieve this goal. As is the case in many technology businesses, we work in small teams introducing new features to a legacy application or extending and maintaining its existing features.
We use a feature branching development methodology, meaning that each of us branches off the mainline of code and works on implementing our new features on our own branches. When we’re done, there is a deploy process involving multiple merges through testing and staging environments before we finally get our code live and consumed by customers.
This process seems like a no-brainer, giving us the confidence to work on our own tasks in isolation and allowing us to control the quality of the code going live. However, maximizing individual productivity over team productivity leads to arduous merges with lots of conflicts, and code reviews are very error prone: No one, however astute, can read through a 1,000-line pull request and spot that one typo in the long self-documenting variable name!
To achieve our goal of releasing and evolving content in an efficient manner, we need to change the way we approach the development process. We need a more simplistic approach that moves the complexity out of the development process. Trunk-based development aims to achieve just that.
Trunk-based development is where all of the developers commit their changes directly to the mainline of code. In Git, that is called the master branch; in Subversion, it is the trunk; and in Mercurial, it’s the head. The mainline must always be kept in a releasable state, meaning that at any point it should be able to be built and released.
Developers will therefore break their work into small incremental chunks, which make small, encapsulated changes to the overall system. Each developer will push their work to the mainline frequently, which ensures they will constantly communicate their work to the rest of their team and release the content to their customers more quickly.
Trunk-based development is effective at cultivating a smoother release strategy within development teams, and it encourages more collaboration, ultimately helping us to release high-quality content and better engage with our customers.
Benefits of Trunk-Based Development
There are many benefits that come with adopting a trunk-based development process:
- The inventory, or stock, of features, which are developed but not yet released to your customer, is cut down, providing the end user with new features more quickly.
- The feedback loop—both between all of the developers and between the business and its end users—is shrunk due to releasing code more frequently.
- The risk involved with pushing to the mainline is greatly reduced when pushing smaller chunks of code. This goes back to Martin Fowler’s motto: “If it hurts, do it more often!”
- The cognitive overhead required to do a piece of work is reduced. When using a feature branching model, there are many tasks that are required, such as making a branch, merging, rebasing, making pull requests, and deleting the branch. Each of these distracts us from actually doing the work we’ve set out to do.
Tools and Techniques to Help Transition to Trunk-Based Development
You might be apprehensive about switching to a trunk-based development approach, especially if you are working on a legacy application or working within a large team of developers with a wide range of experience.
The risk of deploying more anomalies to production can be introduced if the mainline is opened up for all developers to push directly and pull-request-based code review is removed. However, there are a number of tools and techniques that alleviate the risks while allowing you to reap the rewards.
1. Develop a Robust, Automated Build Pipeline
The build pipeline is an automation of your process for packaging and releasing code to your end users. The first step in a build pipeline is the commit build. The commit build will check out the mainline code, compile, run unit tests, package a binary, and upload the result to an artefact repository. Only code that passes this commit build will be deployed to master, but there can be a number of subsequent stages in the pipeline to further test your code-base.
By using a component architecture to assemble your pipeline, you can incrementally develop and strengthen it while the application grows. For example, you may wish to introduce components to perform static code analysis, smoke tests, or automated acceptance tests.
The components should be arranged in ascending order of runtime, starting with the commit build and unit tests. As soon as the commit build has passed, the developer can move on to their next task, leaving the rest of the pipeline to execute.
However, as soon as any component in the pipeline fails, it is important that the mainline is fixed immediately. This should also prompt a new unit test to be written so that you will receive that feedback sooner in future builds.
2. Develop a Comprehensive Suite of Tests
A large part of the build pipeline is the test suite, which can indicate whether the commit will introduce a bug. In an ideal world, the test suite will completely cover the code-base, meaning that if any defects are introduced they will be brought to the attention of the developers with a failing test. But in the real world, this is completely unrealistic.
However, using a few different types of test can help us greatly increase our coverage:
- Unit test – Test individual units or components within your system in isolation. These are the fastest tests, and their failures provide a high level of detail on the location of the defect.
- Gold Master test – Capture the result of a given process within your application, and make sure that your code can recreate the result correctly. These tests can be particularly effective if you have a lot of important but complex business logic in your system.
- Smoke test – Check whether the core functionality of the system is operational. These are nonexhaustive tests. They provide little depth but can tell us whether the important parts of our application are working. An example would be ensuring that the application can be launched or that the interface is responsive to input.
- Automated Acceptance tests – Take a real world use case and check that the application responds as we would expect. These are the slowest types of tests by far, but they can be used to test edge cases of the application.
3. Refactor Your Code Base Where Needed
One of the big things that holds us back from writing unit tests is seeing overcomplex, untestable code—but it doesn’t have to stay that way. As an application evolves, we need to continually revisit the important logic in the system to refactor and improve its design.
A key technique that can help us refactor more effectively, especially when working in a trunk-based development process, is “branch by abstraction.” Branch by abstraction is where you create an interface or abstraction for the class that you wish to refactor, allowing you to work on the details of the new solution without affecting existing function calls. When you are happy that your new solution is working, you can slowly switch the interface to call your new implementation.
It is important to keep revisiting the design and architecture of your system and how that will evolve as the application and its users change.
4. Paired Programming or Mob Programming
Many people might see the loss of code review as a major downside of trunk-based development. However, it encourages us to seek out opportunities for code review much earlier in the development process.
Paired programming, for instance, is when two developers design and implement a solution at the same time, with one acting as the driver and writing the code while the other acts as the passenger and actively reviews it. Mob programming is similar, but with more than one passenger.
The main benefit of paired/mob programming is that the feedback that we once got through code review is now being injected into the development process much sooner. This leads to better, more robust solutions. Additionally, knowledge is shared and solutions are more collaborative when more people get involved in the programming phase. It’s all about maximizing team productivity over individual productivity. For the key features of our applications, mob programming is very effective, and for smaller or less important tasks, paired programming can be more efficient.
Reap the Rewards of Trunk-Based Development
Trunk-based development aims to make your development team more effective at releasing quality content to your customer. As a bonus, it increases positive feedback and collaboration both within your team and with your end user.
By introducing and developing a build pipeline and test suite while cultivating a culture for code refactor and peer programming, you can effectually transition your team to use trunk-based development and reap these rewards.