Splitting shared config into fragments Jump to heading

Some files are conflict magnets because everyone has a reason to touch them: the route table every feature adds a line to, the feature-flag list, the dependency-injection registry, the CI matrix, the permissions map. Each change is tiny and independent, but they all land in the same region of the same file, so any two branches in flight conflict. The fix is structural, not procedural: split the one file into a directory of small fragments, one per feature or owner, and have the application or build assemble them. Two branches that add different routes then add different files, and Git has nothing to merge. This page shows how to make the split without breaking anything, within conflict prevention by design.

When to use this approach Jump to heading

  • One configuration file appears in most of your merge conflicts β€” detecting conflict-prone files from history tells you which.
  • Edits to that file are additive and independent: entries are added or removed, rarely rewritten.
  • The tool that reads the file can load a directory, or you can add a small build step that merges fragments.
  • Ownership of entries maps to teams or features, so fragments have natural names.

Step 1 β€” Confirm the file is a conflict hotspot and its edits are independent Jump to heading

Before restructuring, check both conditions. Count how often the file is changed by concurrent branches, and look at what a typical change does.

# How many merges in the last 6 months touched this file on both sides?
for m in $(git rev-list --merges --since=6.months main); do
  base=$(git merge-base "$m^1" "$m^2")
  if git diff --quiet "$base" "$m^1" -- config/routes.yml; then continue; fi
  git diff --quiet "$base" "$m^2" -- config/routes.yml || echo "$m"
done | wc -l

# What do typical changes look like? Mostly single-entry additions?
git log -20 --format= --numstat -- config/routes.yml | awk '{print $1"+ "$2"-"}' | sort | uniq -c

If most changes add or remove a handful of lines, the file is a good candidate. If changes routinely restructure the whole file, fragmenting will not help much.

One shared file against a fragment directoryWith one shared file, every feature edits the same region and any two branches in flight can conflict. With a directory of fragments, each feature adds its own file, so concurrent branches touch different paths and merge without conflict.config/routes.ymlconfig/routes.d/*.ymlnew feature addslines to one filea new filetwo branches in flightlikely conflictno overlapownershipwhole fileper fragmentload orderimplicitexplicit, by nameGit cannot conflict on files that only one branch touches

Step 2 β€” Choose how fragments are assembled Jump to heading

There are two ways to turn a directory into the configuration the application reads: teach the application to load a directory, or merge the fragments at build time into the single file it already expects.

# Option A: the application loads every fragment in name order
import glob, yaml
routes = []
for path in sorted(glob.glob("config/routes.d/*.yml")):
    with open(path) as f:
        routes.extend(yaml.safe_load(f) or [])
# Option B: a build step concatenates fragments into the file the app already reads
yq eval-all '. as $item ireduce ([]; . + $item)' config/routes.d/*.yml > build/routes.yml

Name order matters when entries are order-sensitive β€” route matching, middleware chains. Prefix fragment names with numbers (10-auth.yml, 50-billing.yml) so the order is explicit and visible in a directory listing.

Step 3 β€” Split the existing file in one mechanical commit Jump to heading

Make the split a commit of its own, containing nothing else, so reviewers can verify it is purely mechanical and later bisects can skip it.

mkdir -p config/routes.d
# Split by top-level key or by owning team; here, by the 'team' field of each entry
yq -o=yaml '.[] | select(.team == "billing")' config/routes.yml > config/routes.d/50-billing.yml
yq -o=yaml '.[] | select(.team == "search")'  config/routes.yml > config/routes.d/60-search.yml
git rm -q config/routes.yml
git add config/routes.d
git commit -m "Split routes.yml into per-team fragments (no behaviour change)"
# Verification: the assembled result equals the old file, entry for entry
git show HEAD^:config/routes.yml | yq -o=json 'sort_by(.path)' > /tmp/before.json
./scripts/assemble-routes.sh | yq -o=json 'sort_by(.path)' > /tmp/after.json
diff /tmp/before.json /tmp/after.json && echo "identical"
Migrating a hotspot file to fragmentsConfirm the file is a hotspot with additive edits, choose whether the application or the build assembles fragments, split the file in a single mechanical commit, verify the assembled result is identical, then tell open branches how to move their edits.Confirm hotspotboth-sides mergesAssemblyapp loads dir / buildSplitmechanical commitVerifyassembled == oldMigrate branchesmove their editsthe split commit should contain no other change, so it is easy to review and to skip

Step 4 β€” Help open branches move their edits Jump to heading

Branches that edited the old file will conflict once, with a modify/delete conflict on config/routes.yml. Give their authors a short recipe: take the split version, then add their entries as a new fragment.

git merge origin/main
# CONFLICT (modify/delete): config/routes.yml deleted in origin/main and modified in HEAD
git diff "$(git merge-base HEAD origin/main)" HEAD -- config/routes.yml   # your additions
git rm config/routes.yml
$EDITOR config/routes.d/55-exports.yml       # put your additions here
git add config/routes.d/55-exports.yml && git commit --no-edit

This is the last conflict the file will cause. Resolving modify/delete conflicts in general is covered in resolving rename and delete conflicts.

Step 5 β€” Keep fragments from re-centralising Jump to heading

Without a rule, the next large feature will add twenty entries to an existing fragment, and that fragment becomes the new hotspot. Add a lightweight check and assign ownership.

# CODEOWNERS
/config/routes.d/50-billing.yml  @acme/billing
/config/routes.d/60-search.yml   @acme/search
# CI: warn if any fragment grows beyond a reasonable size
for f in config/routes.d/*.yml; do
  n=$(yq 'length' "$f"); [ "$n" -gt 40 ] && echo "::warning file=$f::$n entries β€” consider splitting"
done
Merge conflicts on routing config before and afterAn illustrative quarter on either side of the split. With one shared file, routing configuration was involved in a large share of merge conflicts. After splitting into fragments, the only conflicts were the one-time migration of open branches.conflicts involving routing config per month (illustrative)month βˆ’314month βˆ’217month βˆ’112split month9month +10month +20the spike in the split month is open branches migrating β€” once each

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Doesn’t this make the configuration harder to read? Jump to heading

A single file is easier to scan; a directory is easier to change in parallel. Most teams find that a well-named directory, plus a command that prints the assembled result, gives them both.

What about files where order matters within entries? Jump to heading

Fragment boundaries must not split an order-sensitive sequence. Keep each ordered group within one fragment and order fragments by numeric prefix.

Is a union merge driver an alternative? Jump to heading

For append-only files such as a list of entries where duplicates are harmless, merge=union in .gitattributes avoids conflicts without restructuring, as in union merge for append-only files. It does not handle removals or edits safely, so fragments are the more general fix.