TL;DR
- A monorepo only pays off if the build system understands dependency graphs and caching; otherwise it just centralizes pain.
- I use monorepos when cross-cutting changes, shared UI infrastructure, and coordinated refactors matter more than repo-level isolation.
- The highest-value design questions are ownership, boundaries, affected-only CI, and release strategy — not folder structure.
- For React and Next.js organizations, the monorepo choice is really a platform choice: Nx, Turborepo, or Bazel-level rigor.
Context
I think about monorepos as an engineering productivity tool, not a badge of maturity. For frontend platforms, design systems, shared API clients, and shell/domain app contracts, a monorepo can remove a lot of version churn and reduce coordination cost. But the repo shape is not the hard part. The hard part is making builds, tests, releases, and ownership scale without turning every pull request into a rebuild of the world.
The Approach
1. Start with the decision, not the ideology
I use a monorepo when I need:
- atomic changes across shared libraries and apps
- consistent linting, testing, and CI policy
- easy discovery of shared code and architecture patterns
- faster migrations across many consumers
I do not use a monorepo just because “Google does.” If teams ship mostly independent products, share very little code, or have strict access boundaries, a polyrepo can still be the right choice.
2. The build system is the architecture
The monorepo succeeds or fails on whether the tooling can answer:
- what changed?
- what depends on it?
- what can be reused from cache?
- what must rebuild or retest?
For JavaScript and React-heavy repos, my defaults are:
- Nx when I want dependency graph intelligence, generators, and stronger boundary tooling
- Turborepo when I want a lighter workspace with fast caching and fewer conventions
- Bazel only when scale and polyglot depth justify a platform investment
3. Boundaries matter more than package count
I do not split libraries just to make the repo look “modular.” I want packages that map to:
- a real domain boundary
- a platform concern
- a design system layer
- a shared integration contract
Bad monorepo pattern:
utils-a,utils-b,helpers-core,shared-common,common-core
Better monorepo pattern:
design-systemauth-contractscheckout-domainsearch-bff-clienttelemetry-platform
4. CI should be graph-aware, not brute force
My preferred CI model is:
- lint changed packages
- test changed packages plus dependents where needed
- build only affected apps
- run a thin set of repo-wide policy checks
That keeps the repo from teaching engineers to fear broad changes.
5. Ownership has to be explicit
A monorepo without ownership rules becomes a shared attic. I want:
- clear owners per app and library
- conventions for public APIs between packages
- rules around dependency direction
- review policy for platform/shared code
If every app can import everything, the repo silently becomes a distributed monolith inside one Git tree.
6. Release strategy should fit the consumers
I separate three release modes:
- apps release independently
- internal libraries usually release by merge and affected rebuild
- contract packages evolve carefully with additive changes first
That lets me keep the code in one place without forcing one giant deployment train.
Trade-offs
- Monorepos make cross-cutting change easier, but they can make CI, ownership, and Git operations harder if the tooling is weak.
- Shared visibility helps engineering quality, but it also increases the need for standards and review discipline.
- A monorepo reduces internal version drift, but it does not automatically reduce architectural coupling.
- For small teams, a modular monolith in a simple repo often beats a highly engineered monorepo stack.