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.

Tech Talk News Editorial10 min read
ShareXLinkedInRedditEmail
Monorepo vs Polyrepo: When One Repo Actually Wins

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

A monorepo is one repository holding many projects. A polyrepo gives each project its own repo. Monorepos make cross-project changes atomic and code sharing trivial, but they need real build tooling to stay fast. Polyrepos give clean boundaries, independent versioning, and simple permissions, at the cost of a coordination dance every time a change spans services. Pick based on your org shape, not on who else does it.

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.

a typical monorepo layoutplaintext
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 config
apps consume packages, services deploy independently, and one commit can touch all of them at once. The build tool config at the root is what keeps it from grinding to a halt.

That 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

The cost of a cross-cutting change in a polyrepo doesn't grow with the size of the change. It grows with the number of repos the change touches. A one-line fix that spans eight services is eight PRs and a version-bump chain, the same as a huge feature would be. Monorepos collapse that to one diff. If your work routinely crosses service boundaries, this is the axis that dominates everything else.

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

  1. Atomic cross-project change
    N PRs
    1 commit
  2. Code sharing & discoverability
    high friction
    trivial
  3. Boundary enforcement (built-in)
    enforced by default
    you must add it
  4. Build tooling required to scale
    low
    high
  5. Independent versioning & deploy
    free
    needs release tooling
Neither column is the winner. The bars just show that the pain moves. Monorepos win the top two and lose the bottom three by default, and the bottom three are exactly what you buy back with tooling and process.

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]

turbo.jsonJSON
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {}
  }
}
The "^build" tells the tool to build a package’s dependencies before the package itself. The outputs are what gets cached. With this in place, a commit that only touches the admin app rebuilds the admin app, not the entire repo.

Heads up

If you adopt a monorepo and skip the build tooling, you get all of the downsides and none of the upside. Slow CI, bloated checkouts, and nervous engineers, without the atomic-change and sharing benefits ever paying off. The tooling is not a nice-to-have you bolt on later. It is the price of entry. Budget for it on day one or don't go monorepo.

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

The honest tiebreaker is to look at your last fifty pull requests. If a lot of them would have spanned multiple repos under a polyrepo, that's your coordination tax, and a monorepo erases it. If almost none of them cross boundaries, you're paying monorepo tooling costs for atomic changes you never actually make. Let your real change patterns decide, not a blog post and not Google's org chart.

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.

Written by

Tech Talk News Editorial

Tech Talk News covers engineering, AI, and tech investing for people who build and invest in technology.

ShareXLinkedInRedditEmail