Splitting a monorepo: why builds and permissions break
While a repository contains one product, a single tree saves time: shared libraries, a unified style, one run of checks for all changes.
As soon as several services and platforms appear alongside it, codebase growth starts to outpace the team's capacity: an edit in one module triggers the build and tests of its neighbors, and the incremental build cache collapses from someone else's change in shared dependencies. On large trees, a full run takes hours, and this directly hits release frequency.
The second bottleneck is repository checkout and access separation. The full history accumulated over years weighs tens of gigabytes, and a new developer machine or build agent spends more time on it than on the actual work.
Git limits permissions at the repository level, not the directory level, so the choice is small: either everyone sees all the code, including payment and administrative modules, or the build goes through service accounts with manually maintained exceptions. Both extremes are costly — in speed and in risks.
The third reason is commit history. During a split, it must move together with the code: line authorship analysis and task links are needed to investigate incidents and find the cause of regressions, rather than reconstructing context from memory.
Next we break down how splitting a monorepo addresses all three growth points — from preparing the tree and permissions to a way to preserve history.
What splitting gives: build speed and independent releases
In a monorepo, any change triggers a build of the entire tree: an edit in one service pulls dependency installation and checks for all the others.
After the split, the pipeline builds only the repository of the changed service — build time drops from tens of minutes to a few, and the build queue stops being a bottleneck where all teams wait at once.
Independent releases are a consequence of the same boundary. A tag in a service repository triggers only its pipeline, so the class of failures "one team broke the common trunk and blocked everyone's release" goes away.
Each team releases its part on its own schedule, and version compatibility is fixed by contracts and dependency versions, not by a common branch that everyone must keep green.
Reduced recovery time comes from the same property: rollback affects one repository and one tag, not a rebuild of the entire codebase.
The blast radius of a failure is limited to the service — an unsuccessful release is rolled back in minutes, without waiting for other teams to bring the common trunk back to a working state.
The access perimeter also narrows: permissions are issued at the repository level, not for the entire codebase, so a contractor or a new team sees only its own directories, its own secrets, and its own branch protection rules.
Team responsibility zones become explicit — the repository owner is responsible for its build, release, and on-call, rather than dissolving in the common flow of others' changes.
Monorepo split approaches: comparison and limitations
The choice of approach is determined by two questions: whether the subproject needs back-contribution to the monorepo and whether the development lines are separated for long.
A full split, partial extraction with mirroring, and one-time export give a different balance between clean boundaries and the cost of sync maintenance.
History in any approach is transferred with authorship and dates — it is lost only with explicit commit "squashing". Therefore limitations are chosen not by the volume of history, but by how often changes go both ways.
| Approach to subproject transfer | When suitable | Limitations of the approach |
|---|---|---|
| Full split into separate repositories | teams and release cycles diverge, shared code stops being shared | reverse build requires manual setup, cross-cutting links and shared dependencies break |
| Partial subproject extraction with mirroring (git subtree) | subproject is needed both inside the monorepo and outside, back-contribution is expected | double commit history, manual sync, conflicts grow with edits on both sides |
| One-time history export (splitsh, git filter-repo) | a snapshot is needed for an external team or publication | one-way flow: changes from outside return as patches |
Before choosing, history and links are checked on a test clone: only this shows what the tool does with merge commits and signatures.
Work order: from history audit to scheme handover
Before moving anything, we reconstruct the repository picture: which directories develop separately, where history is mixed with edits from neighboring modules, which branches and tags are still active.
A history audit immediately shows which split scheme will hold and which will produce duplicate commits and broken file links.
- Audit of history and active branches. We go through directories and merges, mark long-lived branches and release tags — this determines the boundaries of future repositories.
- Repository scheme design. We fix what is extracted separately, what code remains shared, how versions are linked, and who is responsible for each piece.
- Transfer on a repository copy. We run git subtree or splitsh on a clone: history moves with authors and dates, while the source repository continues to build.
- Integrity check. We compare the number of commits, branches, and tags, compare contents by checksums, and build the project from the new repository.
- Build setup. We move build jobs to new addresses, configure triggers and access permissions so the release passes without manual steps.
- Handover of regulations and training. We hand over the repository scheme, build job description, and branch workflow rules, and walk through them with the team.
If the shared repository continues to live, we separately describe how edits return to it and how this is tracked.
Handover kit: configurations, schemes, regulations
A monorepo split ends not with the first successful push of changes, but at the moment when your team itself builds any extracted repository and, if needed, repeats the split on a new branch. Therefore we hand over not only the result, but everything by which it is reproduced.
- Split configurations — a set of filters for git subtree split and a manifest for splitsh: prefixes, branches, path transfer rules. A re-run starts with one command.
- Dependency schemes — a map of modules and shared libraries: what moved, what remained in the monorepo, and where the source of truth is now for each shared part.
- Access policies — a permission matrix by repositories and branches: who publishes changes directly, who only proposes them, who has the right to forced rollback.
- Verification harness — a scenario that runs the split on a test branch, compares file trees before and after, and stops if the set of paths diverged.
- Documentation — day-to-day work instructions: how to pull changes from the common source and how to transfer an edit from one repository to another.
- Change release regulation — versioning and tagging order, the moment when a repository becomes self-contained, and the reverse build procedure.
- Migration log — a table mapping old paths to new ones and a list of changes after which history in the repositories diverges.
Case: how builds and permissions changed after the split
Initial state
The product team kept everything in one tree Git: platform services, shared libraries, frontend, and configuration files. The full build took about half an hour — the build system rebuilt all modules on any edit, and the shared dependency file reset the cache.
Code access was given entirely or not at all, so external partners sent patches that had to be applied manually.
What was moved
The monorepo was left as the build point, and two repositories were extracted outward via git subtree split while preserving commit history and authorship.
For larger trees, splitsh performs the same transfer faster, but here the standard mechanism and one night of preparation were enough.
Reverse import was made regular: a branch from the extracted repository is pulled into the main tree and goes through review, so partner edits are not lost during the next split.
Metrics before and after
Build dropped from half an hour to a few minutes: only the affected service is rebuilt, not the entire tree and not shared modules. Release conflicts fell from five or six to one per release — edits to shared libraries no longer collide in one commit before rollout.
The share of failures due to someone else's code decreased from one fifth to isolated cases: each team has its own tree and its own checks.
How git subtree differs from splitsh and what to choose
Both paths transfer history, but they manage it differently. git subtree is a built-in mechanism: it extracts a subdirectory into a separate branch without losing commits, and can pull changes back.
Splitsh is a separate utility: it walks the tree, selects the needed paths, and forms a new set of commits, then pushes it to the target repository as a mirror.
The difference is visible in what remains in the target repository. Subtree adds service merge commits on every exchange: history is intact, but the branch becomes overgrown with technical join points.
Splitsh rebuilds commits, preserving author, date, and message — the output is a linear feed. Plus a hard limitation: subtree requires a contiguous subdirectory, while splitsh filters by path list and survives a layout where module files are spread across the tree.
Reverse import is a fundamental divergence. Subtree was designed for two-way exchange: edits from the extracted repository return by subdirectory merge, and conflicts are resolved as usual.
Splitsh works one way: the mirror only receives, and the reverse flow is built separately — with an explicit rule about which source is considered main.
The practical choice is this. Few repositories and live two-way exchange — subtree solves the task without external dependencies. Dozens of subdirectories, a linear feed, and reproducibility from configuration — splitsh is more predictable. An intermediate scenario: splitsh for slicing, subtree or patch exchange for reverse import.
Technical doubts: history, access, reverse import
The most common objection is that when extracting a subproject, commit history will collapse into one service commit and the team's work will be zeroed out.
This is not so: git subtree split rebuilds history only by the specified directory, preserving authors, dates, and messages, and splitsh-lite does the same through a tree cache — a repeated split after new commits takes seconds because previously calculated parts are reused.
The second objection is team downtime and loss of access rights. The directory moves as a separate branch, the build in the new repository is brought up in parallel, and the main flow switches only after a green run.
We issue permissions by the same groups, and the former repository is read-only during verification.
Will commit history definitely be preserved?
We verify before transfer: the number of commits by path, hashes of the latest changes, authorship, and dates. A discrepancy is visible in this check, not a week after migration, when history can no longer be restored.
What happens to build pipelines?
Build descriptions move with the code, and triggers switch to the new repository. The old pipeline continues to work until switchover — rollback takes minutes and does not require rebuilding the build scheme from scratch.
How is reverse import of changes organized?
Edits from a separate repository return to the common one through a separate merge branch: conflicts are resolved at the directory boundary, not across the whole tree, so the merge does not turn into manual review of hundreds of files.
Let's discuss the task: what to prepare for the first conversation
So that a monorepo split estimate is based on facts, not assumptions, for the first conversation it is enough to collect the inputs.
Splitting via git subtree and splitsh solves different tasks: the first preserves history and allows syncing changes back, the second cuts out a directory with full history and authors in one pass.
What to choose depends on the module structure, the links between them, and how build and delivery are organized. Therefore we start not with the tool, but with your repository.
What to prepare for task discussion:
- repository structure: root directories, where shared libraries and service files are located, how modules reference each other;
- list of teams and responsibility zones: who works with which directories and who needs a separate repository;
- history requirements: whether to preserve commits, authors, branches, and tags, whether continuity of release numbering is important;
- links to build and delivery: which build jobs, environments, and artifacts are tied to the current repository;
- dependencies between modules: shared packages, submodules, local paths — everything that will become external after the split;
- access requirements: who should see what after extraction, whether code isolation between teams is needed;
- order and constraints: which module is extracted first, what must not stop during the process.
With this set of inputs, we will return with a work estimate and split plan: the sequence of module extraction, rollback points, and the order of build and access permission edits. Describe the task and send what you already have — we will clarify the rest in conversation.







