Monorepo vs Polyrepo: When One Repo Actually Wins
The monorepo vs polyrepo debate gets treated like a religious war. It isn't. It's a tradeoff about where you want your pain. Here's how to pick without cargo-culting Google.
Every few months someone reignites the monorepo vs polyrepo fight, and it always plays out like a religious war. One side waves Google around. The other side talks about blast radius and bloated checkouts. Both sides act like there's a correct answer carved into a mountain somewhere, and the other tribe just hasn't read the scripture.
I don't think it's that kind of question. The way I think about it, repo strategy isn't about right and wrong. It's about where you want to keep your pain. Both approaches have pain. They just put it in different places, and they bill you for it at different times. A monorepo charges you up front in tooling and discipline. A polyrepo charges you later, in coordination, every single time a change crosses a boundary.
So the real question isn't “which one is best.” It's “which pain can your team actually afford right now.” Let me lay out the axes that matter, because most of the noise online argues about the wrong ones.
Summary
First, the actual definitions
Quick orientation, because people use these words loosely. A monorepo is a single version-control repository that holds many distinct projects: multiple apps, multiple shared libraries, sometimes the whole company. One git clone, one history, one place to look.[1] A polyrepo (sometimes called multi-repo) splits all of that into separate repositories, one per project or service, each with its own history, its own CI (continuous integration), and its own release cadence.
Worth killing one myth right away. A monorepo is not a monolith. A monolith is an architecture: one deployable, one process. A monorepo is a storage decision: where the code lives. You can absolutely run fifty independently deployed microservices out of one monorepo, and plenty of teams do.[2] The two words rhyme and get conflated, but they answer completely different questions.
my-company/
├── apps/
│ ├── web/ ← the marketing site
│ ├── dashboard/ ← the logged-in app
│ └── admin/ ← internal tooling
├── packages/
│ ├── ui/ ← shared component library
│ ├── config/ ← eslint, tsconfig, tailwind presets
│ └── api-client/ ← generated SDK every app imports
├── services/
│ ├── billing/ ← deployed on its own cadence
│ └── notifications/ ← also independent
├── package.json ← workspace root
├── pnpm-workspace.yaml
└── turbo.json ← build graph + caching configThat turbo.jsonat the bottom is not optional decoration. Hold that thought, it's the whole ballgame for the cost section.
Atomic cross-project changes: the monorepo's killer feature
This is the one that actually moves the needle, and it's the one the speed-and-disk arguments bury. Picture a shared library that twelve services import. You need to change its API. Rename a function, tighten a type, fix a bug in a way that changes a signature.
In a monorepo, that's one commit. You change the library and every one of the twelve call sites in the same pull request. CI builds the whole affected graph together. If something downstream breaks, you see it before you merge, in the same diff that caused it. There is no moment in time where the library and its consumers disagree. The question “which version of the shared lib does service X use” literally cannot be asked, because there is only one version, and it's the one in the tree.[3]
In a polyrepo, that same change is a project. You bump the library, publish a new version, then open a pull request in each of the twelve consumer repos to update the dependency and adapt the call site. That's twelve PRs, twelve CI runs, twelve reviewers, and a coordination dance to land them in a sane order. While that's in flight, your services are running a mix of old and new. Some on the new version, some still on the old one. That skew is a feature when the services are genuinely independent and a nightmare when they aren't.
Why this matters
Code sharing and boundaries: a double-edged sword
Monorepos make sharing code trivial. Everything is right there. You import the shared uipackage the same way you'd import a local module, because it basically is one. Discovery is free too. New engineer wants to know how billing handles refunds? It's in the same tree, one search away. No hunting across forty repos, no “wait, which repo is that even in.”
Here's the catch, and it's a real one. Trivial sharing means trivial coupling. When pulling in shared code costs nothing, people pull in everything. You end up with a tangle where a tweak to a “shared” helper ripples into six teams' code, and nobody intended that helper to be load-bearing for half the company. Monorepos don't enforce boundaries. You have to enforce them yourself, with module ownership rules and tooling that actually fails a build when someone reaches across a line they shouldn't.
Polyrepos enforce boundaries by making sharing annoying. To use another team's code, you have to publish it as a package and depend on a versioned release. That friction is a filter. It forces you to think about what's actually a public interface and what's an internal detail. Sometimes that friction is exactly the feature you want, especially when teams are independent and you want hard contracts between them, not a free-for-all.
Polyrepo
Monorepo
- Atomic cross-project changeN PRs1 commit
- Code sharing & discoverabilityhigh frictiontrivial
- Boundary enforcement (built-in)enforced by defaultyou must add it
- Build tooling required to scalelowhigh
- Independent versioning & deployfreeneeds release tooling
Takeaway
Monorepos remove coordination pain and add governance pain. Polyrepos remove governance pain and add coordination pain. You are not choosing between pain and no pain. You are choosing which kind your team is better equipped to absorb.
The cost nobody wants to talk about: tooling
Here's where the cargo-culting falls apart. People point at Google and Meta running everything in one giant repo and conclude that monorepos are the elite choice. What they skip is the part where Google and Meta built enormous custom infrastructure to make it work, because a naive monorepo does not scale.[4]
The failure mode is simple. By default, CI runs everything on every commit. Touch one line in one app, and your CI tries to build and test all forty projects in the tree. At small scale that's fine. At medium scale your pipeline takes forty minutes and engineers start merging on faith. At large scale it never finishes. The whole thing grinds to a halt, and people start whispering that monorepos don't scale, when what doesn't scale is a monorepo without tooling.
The fix is a build system that understands the dependency graph and only builds what actually changed. Tools like Bazel, Nx, and Turborepo do exactly this.[5]They figure out which projects a commit affects, build only those plus their dependents, and cache everything else. Add remote caching so the whole team and CI share build artifacts, and sparse checkout so nobody has to pull the entire tree onto their laptop, and the monorepo stays fast. Google's whole approach rests on this kind of infrastructure, and they wrote a widely cited paper explaining the scale they operate at and the tooling it demands.[6]
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}Heads up
Versioning, releases, and who can touch what
Two more axes that quietly decide a lot of these arguments.
Versioning and release cadence.Polyrepos give you independent versioning for free. Each service ships on its own schedule, has its own version number, and its own deploy pipeline. For genuinely independent products with separate teams, that's exactly right. Your mobile SDK and your internal data pipeline have no business sharing a release train. Monorepos tend toward the opposite default: one version, everything ships together, the state of the tree is the state of the world. That's a lovely property for a single product surface and an awkward one when you're trying to give twelve teams twelve independent deploy buttons. You can get independent releases out of a monorepo, but it takes release tooling and discipline. It's not the path of least resistance.
Ownership and access control. Polyrepos give you clean permissions almost for free. A repo is the unit of access. Want the payments team to own the payments code and nobody else to push to it? Make it a repo and set the permissions. Done. In a monorepo, everyone can see everything by default, which is great for discoverability and bad if you need hard walls. You get there with CODEOWNERS files and path-based access controls, which work well but are something you have to set up and maintain rather than something the structure gives you.[7]If you have strict compliance boundaries or external contributors who should only ever see one slice, polyrepo's permission model is genuinely simpler.
The decision rule, without the dogma
Strip away the tribalism and the actual decision is pretty mechanical. It comes down to two questions: how coupled is your work, and can you afford the tooling.
Go monorepoif you're a small-to-mid org with a lot of shared code and basically one product surface. The atomic changes and trivial sharing remove more pain than the tooling and governance add. A startup with a web app, a dashboard, a shared component library, and a couple of backend services is the textbook case. One repo, one source of truth, refactor across the whole thing in a single PR. The build tooling cost is real but bounded, and Turborepo or Nx gets you most of the way in an afternoon.
Go polyrepo if you have many genuinely independent products or teams with separate lifecycles, or if you flat out cannot invest in build tooling. Independent deploy cadences, clean per-repo permissions, and hard boundaries are worth more to you than atomic cross-cutting changes, because your changes rarely cross those boundaries in the first place. And if nobody on the team is going to own a build graph and a caching setup, a monorepo will just rot into a slow, miserable version of a polyrepo. In that case the boring split wins.
Side note
And please retire the “Google uses a monorepo, so it's best” argument. Google also has thousands of engineers building custom version-control and build infrastructure to make that monorepo survivable.[6]Copy the structure without the infra and you get a giant's problems on a startup's budget. Imitating the shape of a setup while skipping the foundation it stands on is how teams talk themselves into forty-minute CI runs and call it a best practice.
For what it's worth, a project like this blog would be perfectly happy in either. It's one app with a handful of shared utilities. Monorepo, polyrepo, single repo, it genuinely doesn't matter at this size, and anyone telling you it does is selling something. The choice only starts to bite when you have enough projects that the coordination tax or the tooling tax becomes a line item you can feel. Pick the pain you can pay. That's the whole framework.
Sources and further reading
- 1.Primarymonorepo.tools: what a monorepo is and the tooling landscape. monorepo.tools
- 2.Primarymonorepo.tools: monorepo is not a monolith. monorepo.tools
- 3.PrimaryNx: why monorepos, atomic changes and a single source of truth. nx.dev
- 4.ReportingScaling Mercurial at Facebook: the custom infra a giant monorepo needs. engineering.fb.com
- 5.PrimaryTurborepo: task graph, affected-only builds, and remote caching. turborepo.com
- 6.PrimaryWhy Google Stores Billions of Lines of Code in a Single Repository. Communications of the ACM, 2016
- 7.PrimaryGitHub: CODEOWNERS and path-based ownership in a single repo. docs.github.com
Written by
Tech Talk News Editorial
Tech Talk News covers engineering, AI, and tech investing for people who build and invest in technology.