"Why a monorepo?" is usually asked in one of two tones: by a team about to consolidate twelve repositories, or by a team already in one repository and wondering why CI takes forty minutes. Both questions have the same answer, and it is not "monorepos are better." It is that a monorepo trades coordination cost for build cost - and that trade is only good if you actually pay down the build cost.
What a monorepo is, and what it is not
A monorepo is one version-controlled repository containing multiple independently buildable projects - apps, services, and libraries - that share a single commit history and a single dependency graph. That is the whole definition.
It is not a monolith. A monolith is a deployment shape: one artifact, one release. A monorepo is a source-control shape, and it happily contains forty services that deploy on forty independent schedules. Conflating the two is the most common reason teams reject the idea for the wrong reason.
It is also not "put everything in one folder." A monorepo without explicit project boundaries and a tool that understands them is just a large repository, and it will give you every cost below with none of the benefits.
The case for one repository
1. Atomic cross-project changes
This is the reason that survives every argument. In a polyrepo, changing a shared library's API means: land the library change, publish a version, open a PR in each consumer, bump the dependency, land those, and hope nobody was mid-flight. In a monorepo it is one commit and one review, and the change either passes CI everywhere or it does not land.
The practical effect is on refactoring appetite. When renaming a function across nine consumers is one PR instead of ten coordinated releases, teams actually do it. Polyrepos do not make refactors impossible; they make them expensive enough that people stop proposing them, and the architecture calcifies.
2. One version of the truth
A single lockfile and a single toolchain mean every project builds against the same dependency versions. No more "works in service A, breaks in service B because it is two minors behind." Upgrading React, or the TypeScript compiler, or a logging library, becomes a single change with a single blast radius you can see in CI before you merge - instead of a six-week migration tracked in a spreadsheet.
3. Discoverability and honest reuse
Code you can grep is code you can reuse. In one repository, finding the existing auth helper is a search; across twenty repositories, it is a question in Slack that often ends with someone writing a third version. The corollary is also true: dead code and duplicate implementations become visible, because they are all in one place.
4. Shared tooling, configured once
Lint rules, formatter config, TypeScript settings, test setup, CI templates - one place, one upgrade. In a polyrepo, every one of those becomes an N-repository rollout, which in practice means the newest repository has the good config and the oldest has whatever was current three years ago.
5. A dependency graph you can actually use
Because the whole graph is in one place, tools can compute it. That is what unlocks running only what a change affects, ordering tasks correctly, enforcing boundaries ("the web app may not import from the billing service"), and caching task outputs by content hash. None of this is possible when the graph is spread across repositories that only see each other as published version numbers.
What it actually costs
Every one of these is real, and every one of them is a solvable engineering problem rather than a reason to walk away.
Build and CI time grow with the repository, not with your change
This is the big one, and it is the cost teams feel first. The naive monorepo CI job builds and tests everything on every commit, so a one-line README fix pays the same forty minutes as a compiler upgrade. Worse, the number grows every time anyone adds a project, so the pain arrives gradually and is nobody specific's fault.
The fix has two halves: only run what the change affects (an affected graph), and never recompute a task whose inputs have not changed (caching). Get both and CI time tracks the size of your diff instead of the size of your repository. Skip them and the monorepo genuinely is slower than the polyrepo it replaced. We go through the full diagnosis in monorepo build performance.
You must adopt a real build tool
Package-manager workspaces alone will not carry you. You need something that models the project graph and caches task outputs - Nx, Turborepo, Bazel, or Gradle in the JVM world. That is a genuine adoption cost: configuration, a mental model for the team, and discipline about declaring task inputs and outputs correctly.
Ownership needs to be explicit
One repository means one permission surface by default. Code owners, review rules, and module boundary lint rules replace the accidental isolation that separate repositories gave you for free. This is usually an improvement - the boundaries become stated rather than assumed - but it is work you have to do deliberately.
Git and tooling scale becomes a thing
At real scale, clone times, editor indexing, and CI checkout start to matter. Shallow clones, sparse checkout, and partial clone handle most of it, and most teams asking "why a monorepo?" are two orders of magnitude away from the size where this is the binding constraint. Worth knowing about; not worth deciding on.
Monorepo vs. polyrepo, honestly
| Concern | Monorepo | Polyrepo |
|---|---|---|
| Cross-project change | One commit, one review, verified everywhere | Publish, then N coordinated PRs |
| Dependency versions | One lockfile; drift is visible immediately | Per repo; drift is normal and invisible |
| CI cost per commit | Grows with the repo unless affected + cache are in place | Naturally scoped to one repo |
| Tooling requirement | A graph-aware build tool is mandatory | Plain scripts are often enough |
| Isolation and ownership | Explicit: code owners and boundary rules | Free, by accident of repo separation |
| Reuse and discovery | One search; duplication is visible | Requires a registry and social knowledge |
When a monorepo is the wrong answer
- Projects that genuinely share nothing - no common libraries, no coupled releases, different languages and teams. You would be paying the tooling cost for benefits that do not apply.
- Code with a different trust or compliance boundary - an open-source SDK, a customer-specific deployment, or anything with distinct access requirements is often cleaner as its own repository.
- A team unwilling to invest in build tooling - a monorepo without an affected graph and a shared cache converts a coordination problem into a slower, more frustrating CI problem. That is a bad trade.
- Vendored or generated trees you never build - large binary assets and generated artifacts belong somewhere other than your source graph.
The thing that decides it: caching
Notice that most of the costs above reduce to one sentence: in a monorepo, work is shared, so repeated work is expensive. Caching is exactly the mechanism that makes repeated work free.
A task cache hashes a task's inputs - source files, dependency versions, config, the task's own definition - and stores its outputs under that hash. Run the same task with the same inputs again and the tool replays the stored outputs instead of executing. Locally that saves you the second run. Shared remotely across CI and every developer machine, it means the first person to build a given commit pays, and nobody else does - so CI stops rebuilding what a colleague already built, and a fresh branch starts warm.
# The monorepo shape that actually works:
# 1. only what changed
nx affected -t build test lint
# 2. never recompute an unchanged task
# (local .nx/cache + a shared remote cache)
NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://remote.cachely.dev
NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=<your-token>This is why the monorepo question and the caching question are the same question. Teams that consolidate repositories and stop there usually regret it; teams that consolidate and invest in the graph plus a shared cache get the atomic-change benefits without the CI bill. If your inputs are declared sloppily you will still miss - see tuning Nx inputs and outputs for the specifics, and how to improve your CI times for the broader diagnosis.
How to decide
- Do your projects change together? If a typical feature touches two or more repositories, a monorepo removes real coordination cost. If it never does, it will not.
- Do you share libraries? Shared code plus separate repositories means version skew and publish ceremony forever.
- Are you willing to run a graph-aware build tool? If not, stop here. This is the load-bearing prerequisite.
- Can you afford the CI shape from day one? Affected-only execution and a shared remote cache should land with the consolidation, not a year later after everyone has decided monorepos are slow.
If you are unsure where your repository sits today - how the project graph looks, whether boundaries are real, whether your tasks are cacheable at all - the Cachely Repo Check prompt hands a coding agent a read-only checklist and gives you an evidence-backed map before you commit to a direction.
The short version
Choose a monorepo when your projects change together and you are ready to run the tooling that makes shared work cheap. Choose separate repositories when they genuinely do not. And whichever you choose, the deciding variable is not the folder layout - it is whether the same task ever gets computed twice.
Make the monorepo trade a good one
Cachely is a managed remote cache for Nx, Lerna, Turborepo, Gradle, and Bazel - so CI and every laptop share one build memory and nobody rebuilds what someone else already built.