Redesign without fear by Mark Seemann
Avoid merge conflicts with side-by-side changes.
Tim Ottinger published a blog post titled Why redesign doesn't happen (enough). It's a good article that you can read in five minutes, and I recommend that you do. The gist of it is that larger redesigns in established code bases rarely happen because changing too much code in one go risks conflicting with parallel work being done by colleagues.
Tim Ottinger argues that this often leads to merge conflicts, which again reflects poorly on the well-meaning programmer who undertook the work of improvement. His final question is
"What interventions would defang this fear and allow teams to move the design and architecture forward?"
One effective method is side-by-side implementation work.
Add new APIs rather than changing existing code #
As I describe in Code That Fits in Your Head, you can apply the Strangler Fig pattern at the code level, too. Instead of implementing a a new design (say, a new object model) by making direct changes to the existing code, add the new code side-by-side with the old, problematic code.
Once the new design is implemented and works, leave it inert af first, but share it with all coworkers by pushing and integrating it. Since you have only added new files that no-one else uses, there's little risk of merge conflicts.
As a second stage of the process, now that the new design is in place, migrate the problematic code one location at a time by replacing it with simple calls to the new API. You can do this with a granularity of your own choice, thereby minimizing the risk of merge conflicts.
Making sure that the design is right #
One reasonable objection to such a plan would be the following. If you implement a new design, but don't use it at first, then how do you know that it works? How do you know that it actually solves the problem you set out to address?
After all, even with much forethought, it often turns out that your first design doesn't work when you attempt to use it for its real purpose. This is important feedback, so it seems misguided to attempt a redesign without verifying whether it works in reality.
This would be sensible criticism. That's a true problem that you must address.
What you can do is to try it out on the problematic code. Actually perform some of the changes where you replace the problematic code with calls to your new design; what I called the second stage of the process. Do this, however, in a separate Git branch that you keep on your own machine.
Once you have evidence that the new design is right, share that first, but not the 'strangling' commits. You can then later push those extra commits as well, unless the code base has moved so much in the meantime that it's easier to redo the work by moving code over based off of newer snapshots of the repository.
Conclusion #
As I write in Code That Fits in Your Head, making changes to an existing code base is easy if you are a solo developer. When you work in a team, things become more difficult. The good news is that it's a skill that can be learned. Some techniques include patterns like Feature Flag and Strangler Fig, thinking explicitly about versioning, and working deliberately with separating changes of automated tests from changes to the System Under Test.
The book has examples and more details.