Often developers refactor code in their project while their teammates are working on new features or fixing bugs. When this happens, there is always a risk that refactoring and feature branches may quickly accumulate lots of differences and lead to frequent merge conflicts when developers merge their work into master.
It is wasteful and frustrating. Let’s look into four approaches how developers can balance work on features and bug fixing with refactoring to avoid or at least minimise the risk of merge conflicts and rework:
- Entire team can work on refactoring first then move to working on initiatives and bugs.
- Refactor only parts of codebase unrelated to current work in progress.
- Refactor in small steps, frequently merging code into master.
- Keep refactoring in a long-running branch, merging master into it once each feature branch is integrated.
1. Entire team works on refactoring first then moves to working on other tasks.
This approach is the easiest. It works great for small teams of three or four developers, since it is often possible to split refactoring 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 into too many 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 refactor code while some of their colleagues can keep on working 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 master.
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 master into it once each feature branch is integrated.
This approach is much more laborious 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 from one third-party library or framework to another. In other words, everything that can’t be shipped in small 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 master into refactoring branch once a feature branch is merged into master. Once refactoring is done, the refactoring branch could be merged into master 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.