Back to blog
11 min read

How to Prevent Merge Conflicts: A Practical Guide to Merge Conflict Prevention for Startup Dev Teams

Merge conflicts are less a tooling problem than a symptom of how work is shaped and scheduled. This guide gives startup dev teams a practical merge conflict prevention routine: measure where conflicts come from, keep branches and pull requests small, clarify file ownership, and catch parallel work early.

How to Prevent Merge Conflicts: A Practical Guide to Merge Conflict Prevention for Startup Dev Teams

Merge conflicts feel like a tooling problem, but they are mostly a symptom of how work is shaped and scheduled: how long branches live, how big pull requests get, which files everyone touches, and whether people know who else is in the same code. This guide walks you through a merge conflict prevention routine built on those causes: smaller branches, clearer ownership, and early signals that parallel work is about to collide.

You need a Git-based workflow on GitHub or similar, a team that merges through pull requests, and access to your recent merge and PR history.

Step 1: Measure where your conflicts actually come from

Pull the last 30 to 60 days of merged PRs and tag each one that needed a manual conflict resolution. A spreadsheet is enough. For every tagged PR, record the files and directories involved. Recurring names are your hotspots: lockfiles, route registries, shared config, generated code, and large service files are the usual suspects.

Next, record branch age (first commit to merge) and diff size (lines changed and files touched) for conflicted PRs and for a sample of clean ones. In many teams, long-lived or large branches are the common factor, but check your own numbers before building norms around that assumption. You may find that your conflicts come almost entirely from one lockfile and one config file, which calls for a very different fix than "make PRs smaller."

Then separate the conflict types, because they need different remedies:

  • Textual conflicts: two branches changed the same lines, and Git cannot choose. These are the ones that stop a merge.
  • Semantic conflicts: the merge is clean, but the combined result breaks the build or tests, for example one branch renames a function while another adds a new call to the old name. Look for these in reverted merges and red builds on main right after a merge.

Textual conflicts respond to smaller branches, code structure, and syncing. Semantic conflicts respond to testing the merged result, which Step 6 covers.

One mistake to avoid: do not read the tally as a list of who causes conflicts. The person who hits the conflict is usually just the second one to merge. Look for structural hotspots, and keep this baseline, because you will compare against it at the end.

Step 2: Shrink branch lifetime and pull request size

The longer a branch lives, the more main moves underneath it, and the more lines it touches, the more chances it overlaps with someone else. Set a team norm such as merging within one to two days of starting. Treat it as a guideline to tune against your Step 1 data, not a universal rule; some work genuinely takes longer, but it should be the exception you notice.

Smaller PRs usually take planning rather than discipline. Two techniques do most of the work:

  • Vertical slices: ship a thin end-to-end piece of the feature first, then widen it, instead of building the whole backend and then the whole frontend.
  • Preparatory refactors: land the rename, file move, or extraction in its own PR before the feature that depends on it. Mechanical changes like these conflict badly when bundled with logic, and they review quickly on their own.

Feature flags make this practical. A flag lets incomplete work merge to main safely, dark, instead of waiting on a long branch. This is the core idea behind trunk-based development: everyone integrates into one main branch in small, frequent steps. You do not need full trunk-based development to benefit; even a simple config-based flag removes the "it isn't ready to merge" excuse for a week-old branch.

Consider an illustration. Suppose a feature arrives as one 1,500-line PR touching 40 files. It sits in review for days, main moves, and the author resolves conflicts in files they barely changed. Now imagine the same work as three PRs of 300 to 500 lines merged in sequence: first a rename and file move, then the data layer behind a flag, then the UI that turns it on. Each one is reviewed in hours, and each one conflicts with at most a day or two of other people's changes.

If a PR is already large, ask whether any part of it could have merged yesterday. Usually something could.

Step 3: Sync with main early and often

Bring main into your branch at least once a day and always before opening a PR. Small conflicts resolved daily are trivial; the same conflicts accumulated over a week become one tangled decision made under deadline pressure.

You can sync by rebasing or by merging main into your branch. The tradeoff:

  • Rebase: replays your commits on top of main, giving a linear history, but it rewrites your commits and may make you resolve the same conflict repeatedly across several commits.
  • Merge commits: preserve history exactly and resolve each conflict once, but the log gets noisier.

Neither is right for everyone. Pick one and tell the team, because mixing approaches creates confusion. Also note that rebasing does not eliminate conflicts; it only changes when you resolve them.

If you rebase, turn on rerere, which stands for "reuse recorded resolution." Git remembers how you resolved a given conflict and replays that resolution next time it sees the same one:

git config rerere.enabled true

Add --global to apply it to every repository on your machine. It helps most when you rebase the same long branch repeatedly. Still review replayed resolutions, since the surrounding code may have changed.

Two safety rules for rewritten history:

  • Never rebase a branch that others are working from without coordinating first. Their copies will diverge from yours.
  • When you push a rebased branch, use git push --force-with-lease rather than --force. It refuses to overwrite the remote if someone pushed commits you have not seen.

A useful habit: sync first thing in the morning, run the tests, and only then continue working.

Step 4: Reduce conflict hotspots in the code itself

Return to the files that recurred in your Step 1 tally. Some of them can be fixed by changing the code or the repository setup, not the people.

Break up files and patterns that attract edits

Split large files by responsibility so two people rarely need the same one. A 3,000-line service file handling billing, invoicing, and refunds is an obvious candidate. Then look at structural patterns that guarantee collisions:

  • Sorted lists everyone appends to: use one item per line with trailing commas where the language allows, so additions do not touch neighboring lines.
  • Central registries: route tables, plugin lists, and similar files can often be replaced with auto-discovery, where adding a file is enough to register it.
  • Giant switch statements: move to a map of handlers or one module per case.

Handle lockfiles and generated files deliberately

Never hand-merge a lockfile or generated output. Resolve the conflict in the source (for a lockfile, the manifest such as package.json), then regenerate the file with your package manager or generator. Some package managers document their own conflict handling, and some can resolve lockfile conflicts automatically on install, so check your tool's guidance. A .gitattributes merge strategy for these files can reduce friction, but only apply it where the tool's documentation says it is safe, because a wrong strategy can silently produce an inconsistent lockfile.

Stop formatting noise

Adopt a shared formatter and linter, enforce them in CI, and run them in a pre-commit hook. Whitespace changes, quote styles, and import ordering produce conflicts that carry no meaning. If you introduce a formatter to an existing codebase, apply it in a single dedicated PR and merge it when few branches are open.

Step 5: Coordinate who is working on what

Even a well-structured codebase collides when two people unknowingly rework the same area. A little visibility prevents most of it.

  1. Make ownership explicit with CODEOWNERS. This file in your repository maps paths to people or teams, and GitHub uses it to request reviews automatically. For shared areas such as config or core libraries, it also tells everyone who to talk to before changing them.
  2. Announce work in hotspots. Before starting in a file your Step 1 tally flagged, leave a note on the ticket or in the team channel. One line is enough: "Refactoring the auth middleware this week."
  3. Link related issues at planning time. In Linear, relate issues that touch the same area so overlap is visible when work is assigned, not when branches collide. Check Linear's current relation options, as the available link types can change.
  4. Sequence instead of parallelizing. If two tasks must change the same module, run them one after the other, or have one person land the shared groundwork (an interface, a rename, a new abstraction) first so both can build on it.

Ticket scope matters too. A vague ticket like "clean up the settings page" invites a diff that wanders across unrelated files, and wandering diffs overlap with everyone. Tight tickets with a clear definition of done produce focused branches, which conflict less and review faster.

None of this needs a meeting. A short planning check, asking "does anything this week touch the same files?", catches most overlap.

Step 6: Automate checks that catch collisions before merge

Textual conflicts announce themselves. Semantic conflicts do not: two PRs can each pass CI on their own branch, merge cleanly, and break main together. Automation closes that gap by testing what will actually land.

There are two main options:

  • Branch protection requiring up-to-date branches: a PR cannot merge until it includes the latest main, so its tests ran against current code.
  • A merge queue: GitHub's merge queue builds each PR on top of the latest main plus the PRs ahead of it, runs CI on that combination, and merges only if it passes. Confirm current availability and plan requirements in GitHub's documentation, as these have changed over time.

The principle behind both is to run CI on the merge result, not just the branch tip. If you use a queue, make sure your workflows are configured to trigger on queue events, or the checks will never run on the combined commit.

The tradeoff is throughput. Strict "must be up to date" rules force everyone to update and re-run CI each time another PR merges, which becomes a treadmill on a high-volume team. A merge queue mitigates this by doing the updating for you. If your team merges only a few PRs a day, simple branch protection may be enough.

Add one softer guardrail: a CI step that warns when a PR exceeds a size threshold, say a few hundred changed lines excluding generated files. Make it a comment or a non-blocking warning, not a hard failure. It is a nudge toward Step 2, and legitimate large PRs, like a formatter rollout, should still pass.

Step 7: Watch change pressure and stalled work as ongoing signals

Prevention holds only if you notice drift. Track a few leading indicators weekly:

The clearest warning sign is a spike in churn on a shared module while several branches are open against it. That combination means a collision is coming, and it is far cheaper to coordinate now than to untangle it at merge time. You can pull these numbers by hand from GitHub, but it is tedious, which is why it often does not happen. Progress is built for this: it reads GitHub and Linear activity and surfaces change-pressure and stalled-work signals, so a lead sees which work has gone quiet and where merge volume and churn are building without digging through PR lists.

Once a month, review your Step 1 conflict tally and adjust your norms. When a branch has stalled, look beyond the code. A long-lived branch is sometimes a symptom of a blocked or overloaded teammate, an unclear ticket, or a dependency waiting on someone else, and the fix there is a conversation, not a rule about branch age.

Keep the whole routine light: one short recurring review of a handful of numbers, folded into a meeting you already hold. If it becomes a new process, it will be the first thing dropped.

Measuring the difference after one month

Re-run the Step 1 tally after a month and compare it with your baseline: how many PRs needed manual resolution, which files still recur, and how branch age and diff size have shifted. Then tighten whichever norm shows the least improvement, whether that is branch age, PR size, or ownership of a stubborn hotspot. Small adjustments made against your own data will do more than any wholesale new policy.

Learn more about our services


Start your 7-day free trial

Try it on this week's work.

Connect your tools and Progress fills in your last two weeks, so you see what's moving and what's stuck from day one.

7-day free trial · cancel anytime