A small team maintains eleven repositories. A one-line change to a shared logging helper now means a pull request in the library repo, a version bump, then a pull request in every consumer to adopt it, and for a week half the services run the old version and half the new. Someone proposes merging everything into one repo; someone else warns that a monorepo will turn every build into a long slog. Both are right, and that tension is the whole decision.
The takeaway up front: monorepo vs polyrepo is not a question of which is "modern" or "correct." It's a trade between coordination cost and isolation cost. Framed as monorepo vs multi-repo, the same trade holds: a monorepo makes cross-cutting changes cheap and isolation expensive; a polyrepo makes isolation cheap and cross-cutting changes expensive. Pick the model that makes your most common kind of change the easy one, and accept that whichever you choose, you're buying a specific set of problems.
The two models, precisely
A monorepo is one version-controlled repository holding many projects — services, libraries, apps, tooling — developed and versioned together. It is not a monolith: a monorepo can contain dozens of independently deployable services. Its defining property is a single source tree and one commit history for all of it. A polyrepo (or multi-repo) gives each project its own repository, history, release cadence, and usually its own CI pipeline; shared code is extracted into libraries, published to a registry, and consumed by version. The distinction that matters isn't file layout; it's the unit of atomic change, and that single difference drives everything below.
What a monorepo optimizes for
The monorepo's central win is the atomic cross-cutting change. Rename a function in a shared module and update all forty call sites in one commit; the change lands consistent, and review sees the full blast radius in one diff. There's no window where consumers straddle two versions of an internal library, because internally there's only ever one. That cascades into real benefits:
- No internal versioning tax. You don't publish, bump, and adopt internal packages; everything imports from the current tree, so "diamond dependency" conflicts largely disappear.
- One source of truth for tooling. A single linter, formatter, and CI config applies everywhere, so quality rules don't drift repo by repo.
- Frictionless sharing and discovery. Any code is findable and reusable because it's all in one place, a large part of why big engineering organizations favor the model.
The honest cost: a monorepo demands tooling you don't get for free. Naive CI rebuilds everything on every commit, which grows unbearable as the tree does, and without enforced boundaries, "everyone can import anything" quietly becomes "everything depends on everything."
What a polyrepo optimizes for
The polyrepo's central win is isolation and autonomy. Each repo has a hard boundary, enforced by the repository itself rather than by convention: its own permissions, release schedule, and CI that builds only that one thing. A team can own a service end to end without being slowed by anyone else's code. That buys real things:
- Independent release cadence. Each service ships on its own schedule; a risky deploy can't drag unrelated code along with it.
- Naturally fast, scoped CI. A repo's pipeline builds only that repo, so feedback stays quick with no special build tooling.
- Clear ownership and blast radius. Access control and accountability map cleanly to repos, which suits autonomous teams and stricter compliance boundaries.
The polyrepo trade-offs mirror the monorepo's win in reverse: cross-cutting changes get expensive. Updating a shared library means publishing a version, then adopting it consumer by consumer: slow, and prone to version skew where services run different versions of the same dependency for weeks. Discovery is harder, so teams quietly reimplement the same helper three times, and standards drift because nothing forces separate repositories to agree.
The decision framework
Stop asking "which is better" and ask "which problem do I want to have." To decide when to use a monorepo over separate repositories, walk these in order.
- What does your typical change touch? If everyday work routinely spans a shared library and several consumers, a monorepo turns five coordinated pull requests into one commit. If your typical change stays inside one service, the polyrepo's isolation costs you nothing and pays you autonomy.
- How coupled are your projects, really? Tightly coupled code that's always changed and deployed together fights a polyrepo, where the repo boundary doesn't match the change boundary. Genuinely independent products with separate lifecycles fight a monorepo.
- Do your teams need hard autonomy boundaries? Separate release schedules, strict access control, or compliance isolation push toward polyrepo, where the repository is the boundary. Shared ownership and cross-team contribution push toward monorepo.
- Can you fund the tooling the model requires? Monorepo tooling has to test only what changed; a polyrepo instead needs disciplined package publishing. Whichever you skip, that model's failure mode finds you.
- What do your service boundaries look like? Repo structure should echo your service and API boundaries, not fight them. The same reasoning behind a clean, stable contract in REST API design applies: well-defined seams make either model work; muddy seams make both painful.
Coordinated, coupled changes and shared ownership point at monorepo; independent products, autonomous teams, and hard boundaries point at polyrepo. If they point both ways, that's a real signal too, and the next section is for you.
The hybrid most teams land on
The clean dichotomy rarely survives a growing organization. The pragmatic outcome is usually a few repos, each a monorepo for things that change together: group tightly coupled services and their shared libraries into one repo so cross-cutting changes stay atomic, and keep independent products in their own repos so they keep separate lifecycles.
The grouping rule: co-locate what changes together; separate what releases independently. A frontend app and the design-system package it consumes belong together if you change them in the same breath; a payments service with its own compliance boundary and cadence belongs apart. You're not choosing a religion; you're choosing a code organization strategy that draws repo boundaries around real change boundaries. And choose deliberately, because the decision compounds: build automation, access control, and team habits all accrete around your repo structure until switching later is expensive.
FAQ
Is a monorepo the same as a monolith?
No, and conflating them causes most of the confusion. A monolith is an architecture: one deployable unit. A monorepo is a code-organization choice: one repository. A monorepo can hold dozens of independently deployable microservices, and a monolith can live in its own polyrepo. You're choosing how code is stored, not how it's deployed.
Will a monorepo make my builds unbearably slow?
Only if you let it rebuild everything on every commit. The model relies on tooling that detects which projects a change actually affects and builds just those, plus caching so unchanged work isn't repeated. With that in place, CI scales with the size of the change, not the tree; without it, a large naive monorepo grinds to a halt, which is a frequently cited reason teams give up on the approach.
How do polyrepos share code without copy-pasting?
By extracting it into a library, publishing it to a package registry, and consuming it by version. That keeps boundaries clean but adds the versioning tax: each change to the shared library is a publish plus an adopt-it pull request in every consumer, and you must actively manage skew so services don't drift onto incompatible versions.
We're a small team — does this even matter yet?
Less than it will. With a handful of projects either model is fine, so pick the simplest thing that fits how you work — often a single repo, because there's less to run. The decision starts mattering as projects and teams grow and your typical change either keeps crossing repo boundaries or stays inside one. Let that pattern, not headcount, tell you when to revisit.
Next step
Open your commit history from the last month and read what your changes actually touched. If they keep spanning repos and waiting on version bumps, your boundary is in the wrong place and a monorepo would make that work atomic. If they sit cleanly inside one project and you value hard isolation between teams, your polyrepo is earning its keep. Draw repo boundaries around real change boundaries, not around what's trending. More software architecture guidance at TheAppCode.