Source code: Minimizing merge conflicts
Posted: 2025-09-09
Introduction
Teams making fast progress on any shared codebase inevitably face (source code) merge conflicts. This text describes general strategies to improve the situation.
This deliberately doesn’t cover specific tools (such as Git or Mercurial). Teaching a specific tool is not in scope for this document.
Types of conflicts
I distinguish two types of conflicts:
Textual conflicts (the “easy” conflicts): These occur when automated tools can’t reconcile independent changes that affect the same lines of code but where a human can trivially resolve the conflict, typically by selecting the correct combination of lines (or with minor editing). While they don’t require much effort to resolve, they can be very frustrating; they feel like a waste of time.
Logical conflicts (the “hard” conflicts): These are more complex and occur when the changes conflict logically. For example, one change removes a function or modifies an interface that another change still depends on. Resolving these can require careful thought and understanding of both changes.
In my experience, the majority of conflicts are textual. These strategies aim to deal with both types.
Accept conflicts
Conflicts are a normal part of the development cycle, especially easy conflicts. Acknowledging this and adjusting your workflow accordingly goes a long way to mitigate their impact.
Anticipating conflicts helps you reduce the frustration you feel when they occur.
What does this mean in practice?
The value of a change in flight is not the specific artifact you’ve produced (the code change) but the knowledge of all the aspects involved. Be ready to throw away a specific implementation of the change and re-apply it “manually” to a new head.
Small changes
Make each code change as small and focused as possible. Avoid bundling logically separate steps into the same code change. Small changes lets you reduce the impact of conflicts.
Make sure your workflow supports creating chains of dependent changes.
Reduce in-flight work
The probability of conflicts is proportional to the amount of in-flight work (i.e., the number of code changes started but uncommitted). Therefore, reducing the amount of changes in-flight will also reduce the amount of conflicts.
The core principle is to prioritize finishing work over starting new work.
To get changes committed as efficiently as possible, apply these heuristics, in order:
Submit changes as soon as your reviewers approve them (unless they have open “requires action” comments or dependencies on unsubmitted changes).
If change C1 depends on change C0, prioritize C0.
Prefer reviewing other engineers’ code; over replying to review comments on your own code; over advancing work-in-progress changes you have yet to send for review; over starting new changes.
Favor “simpler” less-controversial changes. Get the easy things out of the way. This includes both changes you author and those you review.
Defer work to the future
There’s a balance to be found between:
Polishing code changes to perfection before submitting.
Submitting changes that still have “pending” parts, potential improvements.
The best trade-offs are team- and project-dependent, but submitting code sooner reduces in-flight work and thus avoids conflicts.
This means you should prefer to mark potential improvements with
appropriate TODO comments and get code submitted as soon as
it meets a certain bar of quality (i.e., as long as it doesn’t break
existing functionality).
As a reviewer, be liberal in your approvals. When you spot a potential improvement, distinguish these two situations:
If the code under review is submitted without this improvement, the state of the project regresses in an unacceptable way. In this case, the improvement must be done before submit.
If the code is reviewed as is, the project moves in the right direction. In this case, suggest the potential improvement, but ask for it to be done post-submit: “For a follow-up change, please consider deduplicating the…”
Overcommunicate
A dedicated team channel can prevent many logical conflicts. If you are going to change a significant or potentially disruptive change, announce this.
This is especially critical for changes to widely used interfaces or core logic. A simple message can save hours of rework:
I intend to change interface
WorkflowOrchestratoron Wednesday. Please try to avoid changes that depend on this interface. I’ll let you know as soon as my change is in.
Rebase incrementally
For changes that take a while to be committed, rebase them on head frequently, resolving small conflicts incrementally. This works much better than facing a giant conflict at the end.
Re-apply, not merge
If you’re facing a large textual conflict, remember that the real value of your change is not the specific patch you wrote, but the knowledge and understanding you gained as you wrote it.
This imples that sometimes it’s easier to throw away your change, and “manually” re-implement your solution on head.
Design stable interface
Well-designed software architexture can dramatically reduce conflicts. Aim to build modules with narrow, stable interfaces that change rarely. By allowing different engineers (or sub-teams) to take ownership of different modules, this can have a huge impact in your ability to parallelize work.
Conclusion
Merge conflicts are a natural byproduct of collaborative development, but they don’t have to be a major drain on productivity. By embracing development practices like limiting work in progress, keeping changes small, communicating effectively, and designing stable interfaces, your team can significantly reduce their frequency and impact and focus on what truly matters: building great software.
Related
- Up: Essays