The argument for a monorepo is atomic cross-project changes, one dependency version, and shared tooling. The argument against is that a naive one rebuilds and retests everything on every commit, so the pipeline duration becomes a function of repository size rather than change size. Both are true; which one you experience depends entirely on whether you have a task graph.
The graph is the product
The mechanism is straightforward. Each project declares its dependencies and its tasks, the tool computes a graph, and a change to a leaf triggers only that project and its dependents. A change to a shared library rebuilds what actually depends on it, which is usually far less than everything.
On top of that, cache task results by content hash — of the inputs, the task definition, and the dependency hashes. If nothing that feeds a task has changed, its result is a cache lookup rather than an execution. Shared remotely across CI and developer machines, this is where most of the time goes: a colleague's build already produced the artifact you were about to spend four minutes rebuilding.
{
"tasks": {
"build": {
"dependsOn": ["^build"], // upstream projects build first
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "tests/**", "vitest.config.ts"],
"outputs": ["coverage/**"]
},
"lint": { "inputs": ["src/**", ".eslintrc.cjs"] }
},
"remoteCache": { "enabled": true }
}The inputs list is where teams get this wrong, in both directions. Too broad — a glob covering the whole project — and the cache never hits, because a README edit invalidates the build. Too narrow, and you get a cache hit that skips work which genuinely needed to run, which is a far worse failure because it is silent and intermittent.
Where the model strains
- Repository-wide changes — a linter upgrade, a formatting sweep — invalidate everything by design. Schedule them deliberately, not on a Friday afternoon.
- Version control operations get slow at scale. Partial clone and sparse checkout are not optional past a certain size.
- Ownership needs enforcing, or shared code becomes a commons that everyone edits and nobody maintains. Codeowners plus review rules are structural, not bureaucratic.
- Cache correctness depends on task determinism. A test that reads the clock or the network will poison the cache with a result that was never reproducible.
- Release coordination gets harder, not easier: one repository does not mean one deployable, and versioning independently released packages needs an explicit answer.
A monorepo without a task graph is a polyrepo with worse build times and better import paths.
The decision worth making explicitly is what you are optimising for. If your projects genuinely change together — a shared design system, an API and its typed client, services on one release train — the atomic change is worth a substantial tooling investment. If they are independent products with separate lifecycles, a monorepo buys you coordination overhead in exchange for a consistency you did not need.