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
Conflicts by file, in a typical repositoryA handful of files account for most conflict resolutions. They are almost always registries, configuration maps or generated artefacts rather than ordinary source files, because those are the files every change has to touch.conflicts resolved per file, last twelve monthspackage-lock.json84src/routes/index.ts41config/services.yaml33CHANGELOG.md29everything else, each2four files, three different remedies, none of which is 'resolve conflicts better'

Step 1 — Recognise the Four Collision Shapes Jump to heading

Almost every recurring conflict is one of four, and the remedy differs for each.

Four shapes, four remediesA registry that every feature must edit needs discovery instead of a list. A generated file needs a driver. An append-only log needs union merge. A genuinely shared module needs either splitting or coordination, and only that fourth case is a design conversation.Registryevery feature edits it→ discoveryGeneratedrewritten by both→ merge driverAppend-onlyboth add at the end→ union mergeShared modulereal overlap→ split or coordinatethe first three are removable without touching anyone's code

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
Conflict rate against branch ageBranches merged within a day rarely conflict, because the default branch has barely moved. The rate climbs steeply after about three days and is dominated by files other people were also changing during the same week.share of merges with at least one conflictmerged same day4%1-3 days11%4-10 days34%more than 10 days62%branch lifetime is the single largest controllable factor

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

SignalWhat it usually indicatesRemedy
One file dominates conflict countsA registry or a generated artefactDiscovery, or a merge driver
Many authors per file, few per directoryA file that spans ownership boundariesSplit along ownership lines
Conflicts cluster in one weekA mechanical sweep landedSchedule sweeps, announce them
Conflict rate rises with branch ageLong-lived branchesShorten them; split the work
Same conflict resolved repeatedlyRecurring integration between two linesEnable rerere
Conflicts in generated outputTreated as sourceAttributes and a driver

Troubleshooting Jump to heading

SymptomLikely causeFix
Lockfile conflicts on every branchEvery branch rewrites itMerge driver that regenerates
Route or plugin list conflicts constantlyCentral registryDiscovery, or generate the list
Changelog conflicts on every releaseTwo branches appendingmerge=union
One module conflicts for everyoneOwnership spans several teamsSplit by responsibility
Rebase conflicts repeat every timeThe same resolution, over and overrerere.enabled=true
Conflicts spike after a refactorUnannounced mechanical changeCoordinate 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.