Monorepo Strategy: Nx, Turborepo, and Bazel Guide
Your team migrated six repositories into a monorepo because someone in leadership came from a company that used one. Three months later, CI on main takes 45 minutes. Developers are pushing to feature branches and merging late to avoid triggering the full build. The teams that had been most autonomous before the migration are the loudest about reverting. Nobody set up affected-only task execution. Nobody configured remote caching. The monorepo was not the problem. The missing infrastructure was.
This exact story plays out regularly. The monorepo vs. polyrepo debate has generated far more opinions than evidence. Engineers who’ve worked in successful monorepos are passionate advocates. Engineers who’ve survived dysfunctional ones are equally passionate detractors. Both groups usually describe the same technical structure and draw opposite conclusions. That’s because repository structure does not determine success. Team structure and tooling discipline do.
What Monorepos Actually Solve
The core monorepo advantage is simple and powerful: atomic commits across project boundaries. When your authentication library changes in a way that breaks three services, that change happens in a single PR. Reviewers see the library change and all three service updates together. CI validates the full impact. The library and its consumers are always in a consistent state. There is never a window where the library is on v2 but two services are still running v1 because their teams haven’t gotten to the upgrade PR yet.
In a polyrepo, the same change requires publishing the library, then creating individual PRs in each consumer repository, then waiting for each team to review and merge on their own timeline. During that coordination window, some services run the old version and some run the new one. If the change is breaking in an unexpected way, it surfaces during an individual team’s upgrade, long after the library PR merged, with no easy path back. Anyone who’s managed a breaking library upgrade across 8 repos knows exactly how painful this gets.
The second monorepo advantage is code discoverability. When everything is in one place, engineers search the entire codebase before building something new. That shared utility someone wrote last quarter? It’s in libs/utils and your IDE finds it in seconds. In a polyrepo, it’s in a repository a new team member has never seen, listed in a wiki page nobody has updated since it was created. In practice, monorepo discoverability reduces duplicate implementations of common logic by 30-50% for teams that track it. That’s not a small number.
Tooling Requirements for Scale
A monorepo without task orchestration tooling does not scale past a few dozen packages. Full stop. The naive approach of running all builds and tests on every commit produces CI times measured in hours. The 45-minute CI that frustrated those developers is actually on the mild end. Monorepos without affected-only execution regularly reach 3 hours on main. Migrate to a monorepo without investing in the tooling and you will spend the next year explaining why the migration made everything slower.
Nx, Turborepo, and Bazel all provide affected-only execution. When a PR modifies libs/payments, only packages that depend on libs/payments are rebuilt and retested. Nothing else runs. Remote caching extends this further. If the same inputs produced the same outputs on another machine, the cached result is restored instead of recomputed. Developer laptops and CI runners share the same cache, so a build that passed on a colleague’s machine does not run again in CI. With this infrastructure in place, CI times typically decrease after a monorepo migration rather than increase. Teams report 60-90% CI time reduction from remote caching alone. That’s the difference between a monorepo that works and one that everyone hates.
Bazel offers the most powerful option: hermetic builds with complete dependency declaration and true reproducibility. It is also the most complex to configure and maintain, requiring dedicated platform engineering investment. Nx and Turborepo are significantly easier to adopt for JavaScript and TypeScript projects and support a wider range of project types with less configuration. Start there. Reserve Bazel for monorepos where build reproducibility is a compliance requirement or where the repository has grown past 500+ packages across multiple languages.
Module Boundaries and Ownership
Here is the organizational risk that sneaks up on you. Having everything in one place makes it easy for everything to depend on everything else. A service quietly imports a utility from another service’s internal module. A shared library grows unbounded as teams add domain-specific helpers. Over 6-12 months, hidden coupling accumulates until changes in one area trigger unexpected test failures across unrelated packages. Ownership becomes unclear. Nobody knows who’s responsible for that libs/common directory that 40 packages depend on. And everyone is afraid to touch it.
Module boundary enforcement prevents this at lint time. Nx’s enforce-module-boundaries ESLint rule defines which package types can import from which other types based on tags. A type:app package can import type:feature and type:lib. A type:lib cannot import type:feature. Domain packages can only import from their own domain or shared utility libraries. Violations surface in your editor while you’re typing, not during a production incident six weeks later.
CODEOWNERS files handle review ownership. Teams own their directories. Changes to shared libraries require review from the platform team. Design this deliberately. The ownership model needs to mirror your actual organizational structure, not be retrofitted after teams have already been stepping on each other’s contributions.
Here’s a practical tip that will save you months of pain: set up boundary enforcement before you migrate the second repository. The first migration is too small to feel the need. By the third, the coupling patterns are already established and retrofitting constraints means breaking existing imports. Every team that skips this step regrets it.
When Polyrepo Is the Right Answer
Polyrepo fits well when services have stable, versioned APIs and teams release on genuinely independent schedules. A financial services company where the payments team deploys on a strict change control window and the analytics team ships multiple times per day has genuinely different operational rhythms. Coupling those teams in a monorepo creates coordination overhead that did not exist before. That’s a net negative.
The microservice architecture principle of treating services as independently deployable units maps naturally to independent repositories when interfaces are genuinely stable. The qualifier is “genuinely.” Be honest with yourself here. Many teams believe their services are more independent than their actual frequency of cross-service coordination reveals. If you’re making coordinated changes across 3+ repositories more than twice a month, your polyrepo structure is fighting your actual coupling patterns.
The dependency management problem in polyrepos is solvable with the right tooling. Renovate and Dependabot automate dependency update PRs. Internal package registries with clear semantic versioning make version coordination explicit. The DevOps engineering investment required for polyrepo scale is different from monorepo scale, not necessarily higher. Polyrepos need cross-repo search, dependency dashboards, and release coordination tooling. Monorepos need affected-only CI, remote caching, and boundary enforcement. Total investment is roughly comparable. The question is which coordination problems your teams actually have, not which repo structure a company with 10x your headcount chose.