Why monorepo setup breaks as the team grows
For the first year, a single repository looks like a win: one branch, a shared set of checks, packages reuse each other's code without publishing to a registry.
By the time several teams work on it, release stretches from minutes to hours, and a change in one module rebuilds half the repository.
It's not about the build tool — switching builders doesn't change the picture. The cache is invalidated entirely because nowhere is it recorded which packages depend on what: the build graph grows due to implicit connections through shared utility and type modules.
Build reproducibility disappears — locally the task passes, in the shared environment it fails, and the cause is sought manually in logs.
Checks on every commit take hours because all targets are recalculated, not the affected ones. No one risks running them in parts: without a dependency graph, it's unclear what can really be skipped.
Release time stops depending on the size of the change and starts depending on the size of the repository.
On top of that, manual steps are added: the rollout order of services lives in one engineer's head, versions of shared packages are edited by hand, and environment divergence is caught in production.
We'll break down how this is closed by a dependency graph, explicit package boundaries, and precise cache invalidation.
What a well-established build setup gives: measurable effects
Monorepo setup is measured not by the number of packages, but by the time the team spends waiting and troubleshooting failures. The effects below are visible in metrics already in the first weeks of work.
Build and check time
The dependency graph is built from source files, so only affected projects are rebuilt, and the rest is served from cache. The cache hit rate on a typical change is in the range of 70–90%, and a full run stops being a mandatory ritual before commit.
Checks follow the same graph: linters, types, and tests run only for changed packages and their consumers. Check time drops from tens of minutes to minutes, and the merge queue stops being a bottleneck.
Cost of downtime and eliminated failure classes
The cache is tied to content and environment: if configuration or tool version changes, it invalidates itself instead of serving a stale artifact. The failure class "built locally, failed on the shared branch" goes away.
The second eliminated class is shared library desynchronization: a single dependency registry per repository prevents services from diverging into incompatible majors. Downtime here is measured in hours of a blocked pipeline and in the number of people waiting for a fix.
Hence the fifth effect: release becomes a predictable event with a clear set of changes, and release frequency grows without growth in manual work.
This is not based on our assessment: Google keeps its main code in one repository and applies a shared build graph at the scale of thousands of engineers — the same logic works downward, down to a team of ten people.
Build organization options: comparison of approaches
The orchestrator is chosen based on the dependency graph: how many packages, in how many languages, how often shared libraries change. Below are four working modes, including the option without an orchestrator.
| Option | When it fits | Limitations |
|---|---|---|
| Orchestrator of the Turborepo family | One language, dozens of packages, need local cache and simple configuration without training the team | Package visibility rules are weaker, non-standard build scenarios fit poorly |
| Nx | Multiple applications and shared libraries, need a strict dependency graph and prohibition of cross-cutting imports | Higher barrier to entry: configuration must be maintained and reviewed |
| Bazel | Many languages in one repository, remote cache and distributed build for hundreds of packages | Setup is measured in weeks, requires a dedicated engineer |
| Without an orchestrator | One or two packages, infrequent releases, short build cycle | No local cache: every run rebuilds everything, time grows linearly |
How to choose a mode for team size
For a small team, an orchestrator of the Turborepo family is usually enough: local cache covers repeat builds, and the configuration can be read in full in one evening.
With dozens of packages and several applications, Nx wins — visibility rules prevent an application from pulling in the internals of another library, and the dependency graph remains predictable.
Bazel is justified when packages are written in different languages and a single run stretches to tens of minutes: remote cache and distributed build bring it back to minutes, but require an engineer who lives with that build.
Switching between modes does not have to be one-time: we start with the graph and local cache, and connect remote cache when a run stops fitting into comfortable time.
Monorepo setup: stages of engineering work
We work in stages: first we measure, then we change the structure, and only then do we touch the build and checks. Moving to a single repository without measurements turns into a lottery with releases, so every decision we make relies on before-and-after numbers.
Audit and dependency graph
- Build and dependency graph audit. We measure the full and incremental build, look at which packages pull the entire repository with them, and search for cycles and implicit connections between modules.
- Package boundaries. We mark out what becomes a standalone package and what becomes a shared library, and fix import rules: an application does not reach into the internals of another module.
- Implementation plan. We migrate the repository in parts — first one application with its dependencies, then the rest, without stopping releases.
Build setup and validation
- Orchestrator and cache. We configure Turborepo, Nx, or Bazel for the resulting structure: cache by input content and shared cache for all machines, so unchanged work is not rebuilt.
- Task separation by affected projects. The check pipeline runs tests and builds only for changed packages and those that depend on them — check time stops growing along with the repository.
- Validation on real scenarios. We run typical changes: editing a shared library, release, rollback, parallel branches. Separately, we make sure the cache doesn't serve a stale result.
- Result acceptance and handover. We compare before-and-after measurements, hand over the dependency graph, boundary rules, build documentation, and conduct team training.
What we hand over: configurations, diagrams, policies
Handing over a repository is a state that another team reproduces without the person who set it up. Each artifact below describes not "how it turned out" but why it was done exactly this way and what will break if it changes.
Configurations and diagrams
- Build and cache configuration: explicit task inputs and outputs, scopes of influence, invalidation rules — a repeat run does not rebuild unchanged packages.
- Dependency graph: package graph, module boundaries, prohibition of cyclic dependencies and imports that bypass the public interface.
- Cache access policies: read and write permissions, artifact visibility scope, roles for the release branch and external contractors.
- Local environment description: pinned versions of the platform and package manager, one-command repository bootstrap, set of pre-commit checks.
Policies and documentation
- Project addition policy: criteria for creating a new package, starter template, required configuration fields, review procedure.
- Dependency and tool updates: compatibility check procedure, rollback criterion, migration frequency.
- Build recovery: entry points, typical cache failures, cold build from scratch, artifact integrity check.
- Developer onboarding: first-day route, review of common errors, service owners, and escalation procedure.
All artifacts live in the repository next to the code and are updated in the same review as build changes — otherwise the instructions diverge from reality within a couple of weeks.
Case study: how a failure class in build and release was eliminated
A team from six product units came to us — more than forty engineers in one monorepo for several applications and shared libraries.
The build ran in full on every change: a change in a package with shared types pulled in a rebuild of all products, a full run took about forty minutes, and waiting for a free build agent added up to another half hour. Developers stopped running checks locally, and releases went "by luck."
Situation before
Build cache was formally enabled, but the key was calculated across the entire tree at once, so hits almost never happened: the repeat build rate stayed at 12%, meaning almost every run was calculated from scratch.
Failures came at release, not at check: about a third of releases were rolled back due to build failure or incompatible versions of shared packages. The root was not in the tool but in the scheme — packages had no explicit boundaries, dependencies ran across applications.
What changed after
We split the monorepo into layers — shared libraries, domain packages, applications — and fixed dependency rules: an application can depend on a domain, a domain on a library, the reverse is prohibited and checked automatically.
The cache key was tied to the contents of the package and its dependencies, so unchanged branches of the graph reuse the finished result, and only the affected part is built. The release pipeline was built on Nx with a shared remote cache for all teams.
A quarter later, the measurement was repeated on the second product and compared with public measurements for large monorepos — the order of magnitude matched, meaning the result is reproducible and was not a one-team fluke.
Full build time decreased from 40 to 7 minutes, the repeat build rate grew from 12 to 78%, release failures — from 30 to 3% and remain at that level.
Technical doubts: short answers for engineers
A monorepo is not "putting folders into one repository" but a change in the working model: a single dependency graph, shared build rules, a shared cache of results.
Below are five questions that tech leads most often ask before starting repository migration, and short answers to the point.
How is history transferred from separate repositories?
History is preserved: we transfer not only files but also commits with authors, branches, and release tags — archaeology and blame remain working.
Repositories are connected one by one; the old ones live in read-only mode until the end of migration, so parallel work by teams does not stop.
Will build time increase after consolidation?
Build time grows only if dependencies between packages are declared incorrectly. When the graph is described honestly, only the affected part is rebuilt and checked, and the rest is pulled from cache by input content.
If a rule turns out to be too broad, we narrow the scopes of package influence rather than abandon consolidation.
How do we migrate teams without stopping releases?
Phased transition: first one pioneer team moves its service and tests the rules, then the others connect. New code already goes into the monorepo, and old branches live out their cycle — the switch does not break the release schedule.
How does caching affect repeat checks?
Repeat checks reuse the result if task inputs have not changed — the same set of tests is not run twice on different branches and agents. The remote cache is shared by build machines and local environments, so a developer sees the result already computed on the server.
Who is responsible for build rules after support is handed over?
The rules live in the repository as code, not in someone's head: a change goes through review, and the owner of each area is fixed. When handing over support, we give configurations, the dependency graph diagram, documentation for typical edits, and conduct team training.
Let's discuss your build and release task
To come back with an estimate of the scope of work rather than generalities, engineers need a snapshot of your current build.
It shows where time is lost: on dependency installation, on a full rebuild of packages when one module is edited, or on manual release coordination between teams.
We will assemble this snapshot in one conversation — from you, input data and target metrics; from us, an implementation plan with stages and rollback points.
- Current repository scheme: one shared, a set of separate ones, or mixed; how packages and shared libraries are connected.
- Build bottlenecks: full and incremental build time, share of cacheable tasks, what gets rebuilt without changes.
- Team composition and areas of responsibility: who owns packages, how edits in shared modules are coordinated.
- Release deadline constraints: how often you release, whether there are windows when the build cannot be stopped.
- Target metrics: build and release time, share of paths covered by checks, the failure class that needs to be eliminated.
- Build infrastructure: where agents run — own machines, cloud, or mixed, whether there are access restrictions.
- Current tool (Turborepo, Nx, or Bazel) and what has stopped working for you in it.
Describe the task as it currently is: configuration fragments, build logs, or just a description of the process.
We will come back with an estimate of the scope of work, an implementation plan, and options for transition without stopping releases — we will discuss the task and agree on the next step.
How is a single repository different from separate projects?
Choosing a repository model is not about developer convenience but about how cross-cutting changes propagate through the system.
When a shared module lives in a separate repository, an edit to it turns into a chain of coordinated releases: update the package, wait for builds from all consumers, synchronize versions.
In a single tree, the same edit is checked by one build over the dependency graph, and incompatibility surfaces before it reaches the main branch.
| Option | When it fits | Limitations |
|---|---|---|
| Single repository | Several products share libraries and contracts, cross-cutting changes happen constantly | Build and clone time grows; task cache and per-directory access separation are needed |
| Separate projects | Teams hardly intersect in code and live in their own release cycles | Every cross-cutting change is a series of releases; versions of shared modules gradually diverge |
| Hybrid by domains | There is a core with shared modules and independent products around it | Domain boundaries have to be maintained manually, otherwise you get the same monolith |
Team isolation is solved not by the number of repositories but by access rights and package boundaries: in a single tree, access is granted per directory, and edits in someone else's area are cut off by merge checks.
Tools like Turborepo, Nx, or Bazel add a task cache and dependency graph to the tree — without them, building the entire repository on every change becomes a bottleneck.
The cost of a mistake in shared build rules is the highest: a bad step in a single tree stops all teams at once, so rules are kept next to the code and changed with a separate verifiable change.
We start with a dependency map and the frequency of cross-cutting edits — that is what determines where the boundary lies between the shared tree and separate projects.







