Conflict Prevention by Design Jump to heading
Conflict resolution is a skill worth having and a cost worth avoiding. The conflicts a team actually spends time on are not random: a small number of files produce most of them, for structural reasons that are visible in the history and fixable in the code. A registry file every feature must edit, a lockfile every branch rewrites, a configuration map that grows one line per service — each of these is a designed-in collision point, and each has a standard remedy. This part of Conflict Resolution & Safe Merge Operations is about finding and removing them.
Prerequisites Jump to heading
Measure Before Redesigning Anything Jump to heading
Intuition about which files conflict is unreliable, and restructuring the wrong module is expensive. The data is in the history.
# Files appearing in merge commits most often — a proxy for collision points
git log --merges --name-only --format='' | grep -v '^$' | sort | uniq -c | sort -rn | head -15 # Better: files that were actually conflicted, from rerere's record if enabled
ls .git/rr-cache 2>/dev/null | wc -l # Files changed by the most distinct authors in the last year — the real signal
git log --since='1 year ago' --format='%an' --name-only \
| awk '/^[A-Z]/{a=$0; next} NF{print a "\t" $0}' \
| sort -u | cut -f2 | sort | uniq -c | sort -rn | head -15 Step 1 — Recognise the Four Collision Shapes Jump to heading
Almost every recurring conflict is one of four, and the remedy differs for each.
The first three have mechanical fixes. Only the fourth is a genuine design question, and separating it from the others is what stops a team restructuring a module to solve a lockfile problem.
Step 2 — Replace Registries With Discovery Jump to heading
A file listing every route, handler, plugin or migration is a conflict generator by construction: two features that touch nothing in common both have to edit it.
// Before: every feature adds a line here
import { refunds } from './refunds';
import { invoices } from './invoices';
export const routes = [refunds, invoices /* , and one more per feature */]; // After: the directory is the registry
import { readdirSync } from 'node:fs';
export const routes = readdirSync(new URL('./handlers', import.meta.url))
.filter((f) => f.endsWith('.ts'))
.map((f) => import(`./handlers/${f}`)); # Verification: adding a feature touches only its own file
git show --stat HEAD -- src/routes/ Where a build-time list is required — for bundling, or because runtime discovery is unavailable — generate it and mark it generated, which converts a conflict into a regeneration. The mechanics are in keeping generated files out of merge conflicts.
Step 3 — Split Files Along the Lines People Work Jump to heading
A file that three teams edit weekly is a coordination point regardless of how well it is written. Splitting it along ownership lines removes the collisions without changing behaviour.
# Which authors touch this file, and do they cluster?
git log --format='%an' --since='6 months ago' -- config/services.yaml | sort | uniq -c | sort -rn # After splitting: one file per owning area, composed at load time
ls config/services.d/
cat config/services.d/*.yaml | head # Verification: recent changes now touch one file each
git log --since='1 month ago' --name-only --format='' -- config/ | sort | uniq -c | sort -rn | head The ownership side of this pairs naturally with path-based CODEOWNERS in a monorepo — a file with a single owner rarely conflicts, and a file with five owners almost always does.
Step 4 — Shorten the Window in Which Conflicts Can Form Jump to heading
Two branches can only conflict over changes made while both were open. Halving branch lifetime removes roughly half the opportunity.
# How long do branches actually live?
git for-each-ref --format='%(refname:short) %(committerdate:relative)' refs/remotes/origin | head -20 # And how far do they diverge before merging?
git log --merges --format='%H' -n 50 | while read -r m; do
git rev-list --count "$(git rev-parse "$m^1")".."$(git rev-parse "$m^2")" 2>/dev/null
done | sort -n | tail -5 The techniques for keeping branches short are in trunk-based development setup and splitting a branch into reviewable pull requests.
Step 5 — Coordinate the Sweeps That Cannot Be Avoided Jump to heading
A rename across two hundred files will conflict with everything open. The remedy is scheduling rather than structure.
# Who has open branches that touch the affected paths?
git for-each-ref --format='%(refname:short)' refs/remotes/origin | while read -r b; do
n=$(git diff --name-only "origin/main...$b" -- src/payments/ 2>/dev/null | wc -l)
[ "$n" -gt 0 ] && printf '%3s file(s) %s\n' "$n" "$b"
done | sort -rn | head SAFETY WARNING — do not land a large mechanical change without checking what is open against the same paths. Every affected branch will conflict on every touched line, and the resolutions will be done under time pressure by people who did not make the change. A day’s notice and a quiet window costs far less than twelve hand-resolved rebases, and the procedure is in coordinating a large refactor without conflict storms.
What Prevention Cannot Do Jump to heading
Two people editing the same function for different reasons is not a design flaw — it is two people working on the same thing, and the conflict is doing its job by making that visible. Trying to engineer it away produces either an artificial split that makes the code worse, or an attribute that hides a decision somebody needed to make. The goal is not zero conflicts; it is that every conflict left is a genuine question about intent.
That distinction is worth stating explicitly to a team, because conflict-prevention work has an obvious failure mode: it becomes an argument that any file two people touch should be split. Files exist at the size that makes the code readable, and a module with a coherent responsibility that three people happen to be changing this month is not a collision point — it is a busy month. The measurement in the first section is what separates the two: a file that conflicts persistently across years and across changing personnel is structural, while one that conflicted four times last quarter and never before is circumstantial.
There is also a cost to the remedies themselves. Runtime discovery replaces an explicit list with an implicit convention, which is harder to trace when something does not load. Splitting a configuration file into a directory means the effective configuration is no longer visible in one place. Union merge removes a conflict and, with it, the signal that two people appended at the same moment. Each of those is usually worth paying, and none of them is free — which is why the measurement comes first and the remedy is chosen for a specific file rather than applied as a policy.
Finally, expect prevention to reveal coordination problems rather than remove them. A registry that every feature edits is often the symptom of a system where adding a feature requires touching a central component, and replacing the registry with discovery makes the merge easier without changing that coupling. The conflict was a messenger; it is worth listening to what it said before silencing it.
Configuration Reference Jump to heading
| Signal | What it usually indicates | Remedy |
|---|---|---|
| One file dominates conflict counts | A registry or a generated artefact | Discovery, or a merge driver |
| Many authors per file, few per directory | A file that spans ownership boundaries | Split along ownership lines |
| Conflicts cluster in one week | A mechanical sweep landed | Schedule sweeps, announce them |
| Conflict rate rises with branch age | Long-lived branches | Shorten them; split the work |
| Same conflict resolved repeatedly | Recurring integration between two lines | Enable rerere |
| Conflicts in generated output | Treated as source | Attributes and a driver |
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
| Lockfile conflicts on every branch | Every branch rewrites it | Merge driver that regenerates |
| Route or plugin list conflicts constantly | Central registry | Discovery, or generate the list |
| Changelog conflicts on every release | Two branches appending | merge=union |
| One module conflicts for everyone | Ownership spans several teams | Split by responsibility |
| Rebase conflicts repeat every time | The same resolution, over and over | rerere.enabled=true |
| Conflicts spike after a refactor | Unannounced mechanical change | Coordinate and schedule sweeps |
Frequently Asked Questions Jump to heading
Is it worth restructuring code just to reduce conflicts? Jump to heading
Only when the measurement says the file is a persistent collision point and the restructure is defensible on its own terms. A registry replaced by discovery is usually better code anyway; a module split purely to keep two teams apart frequently is not, and produces an interface that exists for organisational rather than technical reasons.
Does rerere count as prevention? Jump to heading
Not quite — it makes repeated conflicts cheap rather than preventing them, which matters most for long-lived integration branches where the same resolution recurs. It is the right tool when the conflict is genuine and unavoidable, and the setup is in automating repeated conflict resolution with rerere.
How do we know prevention worked? Jump to heading
Re-run the measurement after a quarter. The per-file conflict counts should have shifted, and the files at the top should be different. If the same file still leads, the remedy addressed something else — which is useful information and a reason to look again rather than to try harder.
What about conflicts caused by a formatter? Jump to heading
Those are the easiest category: they are whitespace-only, they have a mechanical resolution, and the prevention is running the formatter in a hook so no unformatted commit exists. The one-off cost of the initial sweep is covered in resolving whitespace conflicts with strategy options.
Should conflict counts be a team metric? Jump to heading
As a diagnostic, yes; as a target, no. The count is useful for finding structural collision points and for checking whether a remedy worked, which is how it is used above. Turned into a goal it rewards the behaviours that suppress conflicts without removing coordination cost: longer-lived private branches, silent overwrites, attributes applied where a person should have decided, and people quietly avoiding files someone else is working on.
The honest framing is that conflicts are a signal about how work is distributed, and the signal is worth more than the number. A team whose conflicts fell because branches got shorter has genuinely improved; one whose conflicts fell because everyone stopped touching the shared module has moved the cost somewhere the measurement cannot see. Keep the count as something you look at when investigating, not as something anyone is asked to reduce.
Related Jump to heading
- Structuring a Codebase to Reduce Merge Conflicts — registries, ownership boundaries and file size.
- Reducing Lockfile Churn in a Busy Repository — the single most conflicted file in most projects.
- Coordinating a Large Refactor Without Conflict Storms — landing a sweep without twelve painful rebases.
- Detecting Conflict-Prone Files From History — the measurement, in detail.