One repository is simple. Many packages are useful. The interesting problem is what to do when both are true at the same time.
That was the situation I found myself in with the mods-framework work. I wanted to keep development cohesive — routes, views, theming, the form layer, the HTTP layer, all together where changes stay coordinated. But I also wanted to publish distinct pieces like form, theme, view, foundation, and http as separate Composer packages that could be consumed independently. If you want the theming layer but not the form system, you should be able to take just that.
Subtree splits are built for exactly this problem. A subtree split takes a directory from a larger repository and produces a separate Git history for just that subtree — a repository that looks to the outside world like it was developed standalone, with its own history and tags, while you continue working in the monorepo. Consumers require the package through Composer and have no idea it was developed inside a larger codebase.
That separation of development structure and distribution structure is the whole point.
Why not just separate repositories from the start
If you split into separate repositories too early, you get clean package boundaries in exchange for significant coordination overhead. Every change that touches multiple packages becomes a multi-repository operation: update the library, tag a release, update the dependency in the consumers. When the packages are still evolving rapidly and their boundaries are not yet stable, that overhead is real friction at exactly the time you need to move fast.
If you keep everything in one repository indefinitely, development stays coherent but reusable pieces get trapped. Someone who wants just the form layer has to take the whole framework. Cross-project reuse requires internal packages, vendored copies, or other fragile workarounds.
The subtree split approach threads between those options. One place to work, multiple publishing destinations. The development experience is monorepo. The distribution experience is independent packages. The transition from “this is internal” to “this is published” can happen package by package as boundaries solidify, rather than all at once.
The tooling
The two tools that come up most often for this workflow are git subtree (built into Git) and splitsh-lite (purpose-built for repeated splitting and much faster on large repositories).
For mods-framework, I used splitsh-lite. The volume of history and the number of packages made the native git subtree approach slow enough that publishing started to feel like something you put off rather than something you do routinely. splitsh-lite makes repeated splits fast enough that it stops being a bottleneck.
The thing that mattered most in choosing tooling: clarity over cleverness. If the publishing workflow is difficult to understand or reason about, the maintenance burden rises fast. You need to be able to debug it when something goes wrong, explain it to someone else, and trust that what gets published is actually what you intend.
What the workflow forces you to do
The most useful thing about setting up a subtree split workflow is not the publishing itself. It is the discipline the workflow demands.
A package that cannot survive a split was not really a package yet. If a directory has dependencies on other directories in the monorepo that are not declared through proper package dependencies, the split exposes that immediately. The published package does not install cleanly, or installs but does not run. That failure is informative — it means the boundary was aspirational, not real.
That forced honesty turned out to be one of the most valuable side effects of the workflow for mods-framework. Several things I thought were independent packages turned out to have implicit dependencies I had not noticed. The split process made those visible and pushed me to either make them proper declared dependencies or rethink the boundary entirely.
Versioning and release coordination
Versioning is where subtree splits stop being just a Git trick and become an operations problem.
Once packages are published independently, there are real decisions about how their versions relate to each other. Coordinated versioning — the whole framework releases at 1.5.0 and every package also gets tagged 1.5.0 — is simpler to operate and easier for consumers to reason about. Laravel itself uses this approach for its components. The downside is that packages that have not changed get version bumps anyway.
Independent versioning lets packages evolve at their own pace. A stable component can stay at 2.0.0 while active development continues elsewhere. The downside is that cross-package dependency constraints become harder to communicate to consumers — which version of mods/form is compatible with which version of mods/foundation?
For a framework still in active development with non-stable APIs, I have been using coordinated versioning with pre-stable version numbers. The version constraint problem is deferred until the packages are stable enough to commit to their own API surfaces. Pragmatic rather than principled, but it acknowledges where the project actually is in its maturity.
When the overhead is worth it
Subtree splits are not free. The publishing workflow requires maintenance, the package boundary discipline is ongoing, and the release coordination adds steps that a simple single-package project does not have.
The overhead is worth it when the package model is real. If parts of the system are genuinely useful in isolation, if there are clear consumers who want different subsets, and if the package boundaries reflect real architectural separations — then the distribution model earns its maintenance cost.
It is not worth it when the package model is aspirational. If you are splitting packages because it sounds like the right architecture rather than because there are real reasons for independent distribution, you end up spending time on release machinery for packages that nobody is consuming independently.
For mods-framework, the calculation was clear enough. The form layer is genuinely useful outside the module system. The HTTP layer has independent value. The view resolution abstractions could apply in systems that are not using modules at all. Those are real reasons to invest in the publishing infrastructure.
What the split workflow gave me, beyond the publishing, was a clearer picture of which parts of the framework deserved to stand alone and which parts were only useful in context. That distinction turned out to matter for the architecture, not just the distribution.