Monorepo Tradeoffs

The argument goes in circles. The monorepo people describe dependency hell, divergent standards, and cross-service refactors that require coordinating seventeen pull requests across seventeen repositories. The polyrepo people describe build systems that take forty minutes, repos so large that git clone is a meaningful time investment, and the uncomfortable moment when a contractor needs access to one service and you realize the only way to give them access is to give them access to everything.

Both sides are describing real problems. Neither side is wrong about the other's failure modes. The debate goes nowhere because it's not actually a debate. It's two groups of people with different current pain points talking past each other.

To make a defensible decision, you need a clear map of what each approach actually costs and buys, and some sense of how those costs scale with your organization.

What the terms mean

A monorepo is a single git repository containing all (or most) of an organization's code. All services, libraries, tooling, and infrastructure configuration live together in one place. A polyrepo is the opposite: each service, library, or component lives in its own repository.

This isn't a binary choice. The space between them is large. A team might have a monorepo for their product's core services while infrastructure tooling lives elsewhere. An organization might have several product-scoped monorepos, one per team or business unit, rather than a single repo covering everything. The relevant question is usually not "monorepo or polyrepo" but "how granular should our repository boundaries be, and where should they sit."

That said, most of the tradeoffs apply regardless of where you draw the boundary, so it's easier to reason about the poles.

What a monorepo buys you

The most concrete benefit is atomic cross-service refactoring. If you rename a function that is called in fifty services, you can rename all fifty callsites in a single pull request. The rename and all the updates land together, or none of them do. There is no period during which version A and version B of the interface coexist in production, no compatibility shim to maintain, no coordinating with the owners of services two through fifty to get their updates merged before yours.

This matters a lot more than it sounds. The ability to refactor aggressively (to clean up a bad abstraction, to change a core data structure, to rename something that was misleadingly named) is the difference between a codebase that stays healthy and one that accumulates load-bearing bad decisions because touching them is too expensive. Polyrepo makes large-scale refactoring expensive. Monorepo makes it just a large PR.

Consistent tooling and standards are a second major benefit. If the build configuration, linting rules, test framework, logging library, and CI setup all live in one place, there's one canonical version of each. Adding a new service means inheriting the existing setup rather than starting from scratch. Updating a dependency means updating it once rather than propagating an update across dozens of repos.

Code sharing also loses its packaging overhead. In a polyrepo, extracting shared code into a library means publishing it as a versioned package: a version number, a package registry, and then updating each consumer separately. In a monorepo, adding a dependency on internal library code is a one-line change in a build file. The library doesn't need to be versioned because the whole tree is versioned together.

Unified CI visibility falls out naturally. One dashboard, one test history, one place to look when something is broken. The operational overhead of monitoring build health across N separate CI pipelines with N separate dashboards is not zero.

What a monorepo costs you

The most immediate cost is build time. Naively configured, a monorepo runs all tests for all services every time anyone touches anything. A one-line change to a README triggers the same CI run as a change to the authentication service. This is obviously wrong, and most teams running serious monorepos fix it with build tools that track the dependency graph and only rebuild and retest what actually changed.

The tools that do this well (Bazel, Nx, Turborepo) work by requiring you to declare what each build target depends on, computing a graph of dependencies, and then only re-executing the parts of the graph that are affected by a given change. Bazel in particular can cache build artifacts by a hash of their inputs, so if the inputs to a build step haven't changed since the last run, it reuses the cached output rather than rebuilding. In a well-configured Bazel monorepo, most CI runs are fast because most things are already cached.

The catch is that "well-configured Bazel monorepo" is doing a lot of work in that sentence. These tools are powerful and non-trivial to configure correctly. The dependency declarations have to be accurate or the incrementality breaks down. Getting a large existing codebase onto Bazel is a significant engineering project. The upfront investment is real, even if the long-term payoff is also real.

Access control is the second major cost. Git doesn't have native support for granting access to a subtree of a repository. If you want to give a contractor access to the billing service, you can give them access to the repo, which means access to everything in the repo. GitHub's CODEOWNERS file lets you specify code review requirements per directory, but it doesn't restrict read access. Actual enforcement requires either a custom solution or a different approach entirely.

At the scale of Google or Meta, the tooling investment required to run a monorepo effectively is enormous: custom version control systems (Google uses Piper, which isn't git), custom build systems, custom code search, custom code review tooling. This is not meant as an argument against monorepos; it's meant as an honest account of what serious monorepos actually require. The tooling that makes a monorepo pleasant at scale is not off-the-shelf.

What a polyrepo buys you

Independent deployments are the headline benefit. Each service has its own repository, its own CI pipeline, and its own deployment cadence. Deploying service A requires no coordination with the team working on service B. The blast radius of a bad deploy is bounded to one service. Changes to service A don't require triggering or waiting on CI for service B.

Ownership is also clearer. This repository belongs to this team. The GitHub team membership for the repo is the ownership model. There's no ambiguity about who's responsible for what, because the repository boundary makes it obvious.

CI is fast because each pipeline only tests one service. There's no build-time scaling problem because each repo is bounded in size. A CI run for service A doesn't have to wait on test results for service B.

Access control is trivial. Grant access to a contractor by adding them to the relevant repository. They can't see other repositories by default. This is the right default for most organizations.

What a polyrepo costs you

Dependency management becomes a recurring tax. Every shared library has a version number, a publish step, and a propagation problem. Service A uses version 1.4 of the authentication library. Service B uses 1.2 because nobody updated it. The authentication library has a security fix in 1.5. You now need to update every consumer, find out which ones broke, get the relevant teams to merge and deploy, and coordinate all of this without breaking anything. This is fine the first few times. It's tiring the fiftieth time.

Cross-service refactors require N pull requests across N repos, often with a compatibility shim in between: maintain the old interface until all consumers have migrated, then remove it in a follow-up PR. Large-scale refactors effectively become infeasible, which means bad abstractions become permanent.

Standards drift is gradual and insidious. Each repo makes independent decisions: this one uses Jest, that one uses Mocha, a third uses Vitest. This one logs JSON to stdout, that one logs plaintext to a file. This one uses ESLint with the Google config, that one uses the Airbnb config, several others have custom configs with local conventions that accumulated over time. None of these decisions are catastrophic. Together, they create a codebase where context-switching between repos costs overhead, and where org-wide improvements are hard to apply consistently.

Discovery is an underrated problem. Which repository contains the code for feature X? In a monorepo, you search once. In a polyrepo, you might need to find the right repo before you can even start searching. With a few repos this is trivial. With hundreds, it's a real friction point.

The scale dependency

Almost every honest post on this topic eventually lands here: the right answer depends on your scale, and the answer that's right today might be wrong in two years.

Polyrepo problems compound with the number of services. Dependency hell with three services is manageable. With three hundred it's a significant engineering problem. Cross-repo refactors with two repos is a minor nuisance. With two hundred it's something you avoid doing, which means bad decisions become load-bearing.

Monorepo problems compound with codebase size and team size. A monorepo for a five-person team with six services is just a well-organized repo. A monorepo for three thousand engineers requires build tooling investment that is itself a significant engineering effort.

A team of twenty with ten services probably finds polyrepo entirely manageable. The coordination overhead is low, the dependency graph is simple enough to track manually, and the access control story is straightforward. A company of two thousand with five hundred services is living in a different world. The cross-repo coordination costs have probably already become painful. The question is whether the monorepo tooling investment is more or less painful than the current situation.

The actual decision

This debate is not about culture or what Google does. Google's monorepo is not evidence that monorepos are correct. It's evidence that a monorepo can work at extreme scale if you invest an extreme amount of engineering effort in the tooling. Most organizations are not Google and cannot make that investment.

The useful question is: what are your actual current pain points? Are you spending a meaningful amount of engineering time coordinating dependency updates across repos? Are you avoiding necessary refactors because the cross-repo coordination cost is too high? Or are you spending time waiting on CI for changes that touch a small slice of a large repo? Are access control boundaries between teams a meaningful operational need?

Enumerate your actual pain points. Project them at 2x your current scale. Pick the approach that makes the projected pain points more manageable, not the approach that eliminates the pain you don't currently have.

Back to writing