Skip to content
Monorepo vs Polyrepo: Structuring Code for a Growing Engineering Team
Web Development9 min read

Monorepo vs Polyrepo: Structuring Code for a Growing Engineering Team

Scult Team
9 min read

The monorepo-versus-polyrepo debate isn't really about one giant repository versus many small ones — it's about where you want to pay your coordination cost. Here's how to choose deliberately.

Every growing engineering team eventually has the same argument: should our five services and three frontend apps live in one repository, or should each have its own? The debate gets framed as a technology choice, but it's really an organizational one — a monorepo and a polyrepo don't differ in what your code can do, they differ in where the coordination cost of a growing codebase shows up. A monorepo concentrates that cost into tooling and CI complexity. A polyrepo concentrates it into cross-repository dependency management and release coordination. Neither eliminates the cost; they just move it.

We've structured projects both ways depending on team size, deployment model, and how tightly coupled the codebases actually are. Here's the honest version of when each earns its keep. If you're weighing this alongside other engineering-org decisions, our comparisons hub has several more laid out in the same head-to-head format.

What Each Term Actually Means

A monorepo is a single repository containing multiple, often independently deployable projects — several services, a shared UI library, multiple frontend applications — all versioned together in one git history, usually with tooling (Nx, Turborepo, Bazel, or a package manager's native workspaces) layered on top to manage building, testing, and dependency graphs between the pieces.

A polyrepo is the more traditional default: each service or application lives in its own repository with its own git history, its own CI pipeline, and its own release cadence, with shared code distributed as versioned packages published to a package registry (npm, a private registry) rather than living in the same tree.

Neither is inherently about company size — there are single-repo giants (Google's famously enormous monorepo) and multi-repo startups. The right structure follows from how coupled your codebases actually are and how your team is organized around them.

The Case for a Monorepo

  • Atomic cross-project changes. When a shared type, a shared UI component, or a shared API contract changes, a monorepo lets you update every consumer of that change in the same commit, verified by the same CI run. In a polyrepo, that same change requires publishing a new package version, then separately updating and testing every dependent repository — a process that's easy to get out of sync, leaving some services on an old version of a shared library for longer than intended.
  • Simplified dependency management. There's one version of a shared library in use across the whole codebase (enforced, not just hoped for), which eliminates an entire class of "works in service A, broken in service B because they're on different versions of the same internal package" bugs.
  • Easier large-scale refactoring. Renaming a widely-used function, upgrading a major dependency, or restructuring a shared module touches every consumer in one PR that CI can verify in full, rather than a coordinated multi-repository migration tracked across a dozen separate pull requests and release schedules.
  • Better code discoverability. A new engineer can search the entire codebase — frontend, backend, shared libraries — in one place, without needing to know which of fifteen repositories contains the function they're looking for.

The Case for a Polyrepo

  • Independent deployment and access control. Separate repositories make it trivial to grant a contractor or a specific team access to exactly the code they need without exposing the rest of the organization's codebase, and to deploy each service entirely independently without any shared CI configuration to reason about.
  • Simpler CI for each individual project. A polyrepo's CI pipeline only ever needs to know about one project — build it, test it, deploy it. A monorepo's CI needs to be smart enough to figure out which of the many projects in the tree actually changed and only rebuild/retest those, which is a real tooling investment (this is exactly the problem Nx and Turborepo are built to solve, through dependency-graph-aware task caching and affected-project detection).
  • No accidental coupling. In a monorepo, it's easy for a "shared" utility to quietly become a dependency of everything, making it harder to change without touching unrelated projects. A polyrepo's harder boundaries — you have to explicitly version and publish a package to share code — impose friction that, while occasionally annoying, keeps coupling deliberate rather than accidental.
  • Repository size and git performance. A monorepo containing years of history across many large projects can genuinely slow down common git operations (clone, checkout, blame) at a scale most teams never reach, but that does become real at the scale of hundreds of engineers and millions of lines — the reason Google and Meta built entirely custom tooling around their monorepos rather than using off-the-shelf git.

The Tooling Question Is Not Optional for a Monorepo

A monorepo without dependency-graph-aware tooling degrades quickly as it grows — a naive setup that rebuilds and retests everything on every commit becomes slow enough that engineers start ignoring CI failures or waiting twenty minutes for feedback on a one-line change. This is the single most common reason monorepo adoptions fail: teams centralize their code without adopting the tooling that makes a monorepo's CI scale sensibly.

Tools like Nx and Turborepo exist specifically to solve this: they build a dependency graph of which projects depend on which, cache build and test outputs keyed on actual file changes, and only rebuild/retest the projects genuinely affected by a given change (plus their dependents). Adopting a monorepo without adopting one of these — or an equivalent custom setup — is adopting the cost of a monorepo without its actual payoff.

Where Team Size Actually Matters

Team size correlates with the right answer, but indirectly — through how coupled the codebases are, not through headcount alone.

  • A small team (under roughly 15-20 engineers) building a handful of tightly related services — a web app, its API, a shared design system — tends to benefit from a monorepo, because the coordination overhead of cross-repo changes would otherwise fall disproportionately on a small number of people, and the team is small enough that everyone can reasonably understand the whole tree.
  • A larger organization with many teams building genuinely independent products that rarely share code and deploy on entirely different schedules often does better with polyrepos, since the isolation and independent deployment benefits outweigh the atomic-change convenience for code that was never that coupled in the first place.
  • A larger organization with many teams building tightly interdependent services (the scenario Google, Meta, and similar companies are in) tends to converge back on a monorepo despite the scale, specifically because the atomic cross-project change guarantee becomes more valuable, not less, as more teams depend on shared internal libraries — they just have to invest heavily in custom tooling to make it work at that scale.

A Middle Ground: Selective Consolidation

Not every codebase has to be all-in on one model. A common, pragmatic pattern is grouping genuinely coupled projects into a small number of monorepos — a "frontend monorepo" containing a company's web apps and shared UI library, a separate "services monorepo" for backend microservices that share internal libraries — while keeping projects with little real overlap in their own separate repositories. This captures most of the atomic-change benefit for code that's actually coupled, without forcing unrelated projects into the same tree just for the sake of consolidation.

Code Ownership and Review Discipline at Scale

A monorepo removes the natural access boundary a separate repository provides by default, which means code ownership has to be enforced deliberately rather than assumed. A CODEOWNERS file (or equivalent) mapping specific directories to specific teams, combined with branch protection rules that require the relevant owning team's approval before a merge, recreates most of the review discipline a polyrepo gets automatically from its structure — but it has to be configured and actively maintained as the codebase grows, rather than being an inherent property of the repository layout.

Without this, a monorepo's biggest strength — anyone can change anything in one commit — becomes a liability: a well-meaning engineer refactoring a shared utility can touch a dozen other teams' code in the same PR, sometimes without those teams noticing until it's already merged. The teams that get the most value out of a monorepo are the ones that pair it with real ownership tooling from early on, not the ones that treat "everything is technically reachable" as equivalent to "everything is fine to change without asking."

Migrating From Polyrepo to Monorepo (or the Reverse)

It's worth knowing that this decision isn't permanent or irreversible, which lowers the stakes of getting it exactly right on the first attempt. Consolidating several existing polyrepos into a monorepo is a well-trodden path — git history can be preserved and merged using git subtree or similar tooling, and the harder part is almost always retrofitting consistent tooling (a shared linter config, a shared CI pipeline, a shared package manager version) across projects that grew up independently with their own conventions, more than the mechanical act of moving code into one tree.

Splitting a monorepo back apart is rarer in practice but not unheard of — usually triggered by a specific project needing genuine access isolation it didn't need before (a client engagement requiring a fully separate codebase, for instance), rather than by the monorepo itself having failed technically. Knowing the path isn't a one-way door makes it easier to make a reasonable decision now based on today's team shape, rather than over-engineering for a hypothetical future scale that may never arrive.

What We Actually Recommend to Clients

For most small-to-mid-size product teams we work with — a web frontend, a backend API, and maybe a shared component library — a monorepo with Nx or Turborepo, or a package manager's native workspace feature for simpler cases, is the more practical default. The atomic cross-project changes and simplified dependency management outweigh the CI tooling investment at that scale, and the tooling required (Turborepo especially) has a low enough setup cost that it's rarely the wrong first move.

We reach for a polyrepo instead when a project genuinely needs strict access isolation between codebases (different clients' code, or regulatory separation requirements), when different pieces are on entirely independent release schedules with minimal shared code, or when a client's existing organizational structure — separate teams that rarely interact — makes the isolation more valuable than the convenience.

The Practical Takeaway

There's no universally correct answer between monorepo and polyrepo — there's only a better fit for how coupled your codebases actually are and how your team is organized around them. The mistake to avoid is picking based on which is trendier this year rather than on your actual coupling and deployment needs, and — if you do choose a monorepo — skipping the dependency-graph-aware tooling that makes it scale, which is the single most common reason monorepo adoptions end up feeling worse than the polyrepo they replaced.

Want results like this?

Keep reading