Often developers refactor code while their teammates are working on new features or fixing bugs. In such scenarios there is a risk that that refactoring and feature branches may significantly diverge. As a result, developers may have to resolve multiple conflicts when merging their code into mainline, rerun tests and potentially fix regressions.
It is wasteful and frustrating. Let’s see what developers can do to successfully balance feature work and refactoring to avoid or at least minimise those issues.
Four strategies come to mind:
- Entire team does refactoring first then switches to features and bugs.
- Refactor only parts of codebase unrelated to ongoing feature work.
- Refactor in small steps, frequently merging code into mainline.
- Keep refactoring in a long-running branch, merge mainline into it once each feature branch is integrated.
1. Refactor first, then switch to features and bugs.
This approach is the easiest. It works great for small teams of two or three developers, since it is often possible to split work into 2-3 parallel streams. However, it is not always possible for a larger team to focus on refactoring entirely. It may be difficult or impractical to split work in multiple streams unless developers wish to work in pairs.
2. Refactor only parts of codebase unrelated to ongoing feature work.
The second approach is quite simple too but works only if refactoring and feature work can actually be done on different parts of the product. For example, refactoring of backend code in a web application can often happen in parallel to purely front-end feature development. This approach is great for larger teams when several developers can do refactoring and several of their colleagues can work on features. That ensures that developers in each stream can support each other, share learnings and knowledge and no-one in the team is left to work in isolation.
3. Refactor in small steps, frequently merging code into mainline.
When refactoring and feature work deal with the same parts of the codebase, this approach should prevent the refactoring and feature branches from diverging too far. As a result, developers can avoid merge conflicts in most cases. Using this approach, each developer can move forward at maximum velocity as with the previous two ones without creating problems to their peers. This approach works for teams of any size.
4. Refactor in a long-running branch, merge mainline into it once each feature branch is integrated.
The fourth approach is much harder and is probably the only options for refactorings that have to be done in long-living branches, such as UI redesigns, change of database schema, migrations to other third-party libraries or frameworks. In other words, everything that can’t be split into small shippable chunks.
In this case, it is probably best to allow developers who work on features to move at maximum velocity and free them from resolving merge conflicts where possible. Instead, devs who are doing refactoring should merge mainline into refactoring branch once a feature branch is merged into mainline. Once refactoring is done, the refactoring branch could be merged into mainline without merge conflicts.
This approach allows to develop feature and fix bugs at maximum velocity while still allowing other engineers to continue with complex refactoring.
Developers who choose this strategy should remember, however, that the longer a branch lives and the more changes it accumulates the higher is the chance that it will never be shipped. That often happens because of the risk of breaking too many things when it gets merged or because developers have to switch to tasks with higher priority and never get back to that refactoring. I'm planning to talk about that in one of my future posts.
Thanks for reading this. You might want to have a look at my previous post on how to avoid scope creep during refactoring as well.