How Monorepos Work and When They’re Worth the Complexity

Derek Chen

Derek Chen

July 7, 2026

How Monorepos Work and When They're Worth the Complexity

A monorepo is a single version-controlled repository containing multiple distinct projects or packages — a frontend app, a backend service, shared utilities, and design system components all living in one repo rather than spread across four separate repositories. Google, Meta, and Microsoft have famously used monorepos for their internal codebases at enormous scale. More recently, tools like Nx, Turborepo, and Bazel have made the approach viable for smaller organizations, and monorepos have become a mainstream architecture choice for medium-sized engineering teams.

The appeal is real: code sharing between packages is easier, cross-package changes happen in a single commit, dependency versioning is centralized, and CI visibility across the entire codebase is unified. The complexity is also real: build tooling requires more sophistication, repository size grows, and team coordination patterns need to adapt. Whether the tradeoff is worth it depends on specific organizational and codebase characteristics that aren’t universally applicable.

The Core Mechanics: How Monorepos Work

At the simplest level, a monorepo is just a repository with subdirectories for each project. What distinguishes a well-managed monorepo from a directory mess is tooling that understands the relationship between packages — which packages depend on which others — and uses that dependency graph to optimize operations.

Package managers (npm workspaces, yarn workspaces, pnpm workspaces) handle the dependency linking that makes local package imports work without publishing to npm. When `apps/frontend` depends on `packages/ui-components`, the workspace configuration creates a symlink so the import resolves to the local package rather than a registry version. This is the foundation that enables cross-package development without publishing intermediate versions.

Build orchestration tools — Turborepo, Nx, and Bazel for larger scales — sit on top of workspace package management to add two critical capabilities: task graph execution (running builds, tests, and linting in dependency order, parallelizing where possible) and caching (avoiding re-running tasks whose inputs haven’t changed). Without caching, running tests across a large monorepo requires running every test suite on every change, which becomes prohibitively slow. With caching keyed to file content hashes, a CI run that changes only `packages/api-client` only re-runs tests for packages that depend on `packages/api-client`, skipping everything else.

Monorepo dependency graph visualization showing multiple packages and their relationships in a single repository workspace

Where Monorepos Make Development Better

The strongest case for monorepos is cross-package atomic changes. In a polyrepo setup (each project in its own repository), making a breaking API change in a shared library requires: updating the library, publishing a new version, then updating every consumer repository to bump the dependency version — a multi-PR, multi-repo process that creates coordination overhead and a period where consumers are on different versions. In a monorepo, that same change is a single PR that updates the library and all consumers together, the CI runs across all affected packages, and the change either succeeds everywhere or doesn’t merge. This is genuinely better for changes that require coordinated updates across multiple packages.

Code sharing is more natural in a monorepo because sharing doesn’t require publish/update cycles. A utility function that would otherwise be copy-pasted across three apps becomes a `packages/shared-utils` entry that all apps import directly. Type definitions shared between a frontend and backend exist in a single `packages/types` package rather than being manually kept in sync. These benefits compound with codebase size — the more packages share code, the more valuable the monorepo structure becomes.

Developer experience for full-stack changes is better when the frontend and backend live in the same repository. A developer making a product change that requires both a new API endpoint and a new UI component makes both changes in a single branch, can test the full integration locally without pulling from multiple repos, and ships a single PR that reviewers can understand holistically. The cognitive overhead of context-switching between repositories for a single feature is real and is eliminated in a monorepo.

Where Monorepos Create Problems

Git performance degrades with repository size. A monorepo at Google’s scale — famously, a single repository with billions of lines of code — requires custom VCS tooling (Google built Piper) because standard Git wasn’t designed for repositories that large. Smaller monorepos are well within Git’s comfortable operating range, but organizations that accumulate large binary assets (video files, large test fixtures, design assets) in the same repository will encounter git clone and fetch slowdowns that don’t affect well-structured polyrepos. Git LFS (Large File Storage) and `git sparse-checkout` are the practical mitigations, but they add workflow complexity.

CI complexity is the most common pain point in monorepo adoption. Correctly identifying which packages are affected by a change — to avoid running the entire test suite on every PR — requires build tooling that understands the dependency graph and correctly calculates the affected set. When this works, monorepos have excellent CI efficiency. When the affected-package calculation has gaps or is incorrectly configured, teams either run too little (missing test failures) or too much (slow CI from running unaffected tests). Getting this right requires upfront investment in build tooling configuration that polyrepo setups don’t need.

Ownership and access control are harder in a monorepo. In a polyrepo, access permissions are repository-level. In a monorepo, if you want frontend engineers to not have access to the billing service code for compliance reasons, you need a different mechanism — code ownership files (GitHub’s CODEOWNERS), branch protection rules, or third-party tools that implement package-level permissions. For organizations with strict separation requirements between teams or codebases, this is a genuine challenge without a perfectly clean solution.

Split screen showing polyrepo versus monorepo code organization structure, comparing repository management strategies

When It’s Worth It

Monorepos make the most sense when: multiple projects share significant amounts of code, cross-project changes are frequent enough that coordinating across multiple repositories is a recurring pain, the team is large enough that the code sharing and visibility benefits outweigh individual project autonomy, and the team is willing to invest in the build tooling required to make CI efficient at scale. They make less sense for a single-product team with no shared code, a team of three that doesn’t have coordination overhead, or a context where independent deployment of services with fully independent version control is a hard requirement.

The tooling for monorepos has improved dramatically. Turborepo in particular has made monorepo setup accessible enough that smaller teams can benefit without investing heavily in custom build infrastructure — sensible defaults, good caching, and a configuration model that doesn’t require significant DevOps expertise to set up. Nx offers more opinionated scaffolding and more sophisticated dependency graph features for larger-scale needs. Starting with pnpm workspaces and Turborepo covers most needs for a ten to fifty person engineering team without the overhead that made early monorepo adoption painful.

More articles for you