Understanding Monorepos

Monorepos are hot right now, especially among Web developers. We created this resource to help developers understand what monorepos are, what benefitsthey can bring, and the tools available to make monorepo development delightful.

There are many great monorepo tools, built by great teams, with different philosophies. We do our best to represent each tool objectively, and we welcome pull requests if we got something wrong!

The tools we'll focus on are: Bazel (by Google),  Gradle Build Tool (by Gradle, Inc),  Lage (by Microsoft),  Lernamoon (by moonrepo)Nx (by Nrwl)Pants (by the Pants Build community)Rush (by Microsoft), and Turborepo (by Vercel). We chose these tools because of their usage or recognition in the Web development community.

What is a monorepo

Let's start with a common understanding of what a Monorepo is.

Why a monorepo

What are the situations solved by monorepos.

Features of a monorepo

What to expect from a monorepo tool

# What is a Monorepo?

Let's define what we and others typically mean when we talk about Monorepos.

A monorepo is a single repository containing multiple distinct projects, with well-defined relationships.

We at Nrwl think this is the most consistent and accurate statement of what a monorepo is among all the established monorepo tools.

Not just “code colocation”

Consider a repository with several projects in it. We definitely have “code colocation”, but if there are no well defined relationships among them, we would not call it a monorepo.

Likewise, if a repository contains a massive application without division and encapsulation of discrete parts, it's just a big repo. You can give it a fancy name like "garganturepo," but we're sorry to say, it's not a monorepo.

In fact, such a repo is prohibitively monolithic, which is often the first thing that comes to mind when people think of monorepos. Keep reading, and you'll see that a good monorepo is the opposite of monolithic.

# But why?

Let's go deeper into the rabbit hole.

A “Polyrepo”

For the sake of this discussion, let's say the opposite of monorepo is a "polyrepo". A polyrepo is the current standard way of developing applications: a repo for each team, application, or project. And it's common that each repo has a single build artifact, and simple build pipeline.

The industry has moved to the polyrepo way of doing things for one big reason: team autonomy. Teams want to make their own decisions about what libraries they'll use, when they'll deploy their apps or libraries, and who can contribute to or use their code.

Those are all good things, so why should teams do anything differently? Because this autonomy is provided by isolation, and isolation harms collaboration. More specifically, these are common drawbacks to a polyrepo environment:

Polyrepo

Cumbersome code sharing

To share code across repositories, you'd likely create a repository for the shared code. Now you have to set up the tooling and CI environment, add committers to the repo, and set up package publishing so other repos can depend on it. And let's not get started on reconciling incompatible versions of third party libraries across repositories...

Polyrepo

Significant code duplication

No one wants to go through the hassle of setting up a shared repo, so teams just write their own implementations of common services and components in each repo. This wastes up-front time, but also increases the burden of maintenance, security, and quality control as the components and services change.

Polyrepo

Costly cross-repo changes to shared libraries and consumers

Consider a critical bug or breaking change in a shared library: the developer needs to set up their environment to apply the changes across multiple repositories with disconnected revision histories. Not to speak about the coordination effort of versioning and releasing the packages.

Polyrepo

Inconsistent tooling

Each project uses its own set of commands for running tests, building, serving, linting, deploying, and so forth. Inconsistency creates mental overhead of remembering which commands to use from project to project.

A “Monorepo”

We can end up in pretty tricky situations when working in a polyrepo. But how can a monorepo help solve all of them?

Monorepo

No overhead to create new projects

Use the existing CI setup, and no need to publish versioned packages if all consumers are in the same repo.

Monorepo

Atomic commits across projects

Everything works together at every commit. There's no such thing as a breaking change when you fix everything in the same commit.

Monorepo

One version of everything

No need to worry about incompatibilities because of projects depending on conflicting versions of third party libraries.

Monorepo

Developer mobility

Get a consistent way of building and testing applications written using different tools and technologies. Developers can confidently contribute to other teams’ applications and verify that their changes are safe.

# Monorepo features

Everything you need to make monorepos work.

It's not just about the features.

Features matter! Things like support for distributed task execution can be a game changer, especially in large monorepos. But there are other extremely important things such as dev ergonomics, maturity, documentation, editor support, etc. We don't cover them here because they are more subjective.

You may find, say, Lage more enjoyable to use than Nx or Bazel even though in some ways it is less capable.

Some features are easy to add even when a given tool doesn't support it (e.g., code generation), and some aren't really possible to add (e.g., distributed task execution).

# Monorepo tools

How do they compare? let's see how each tools answer to each features.

Fast
Local computation caching

The ability to store and replay file and process output of tasks. On the same machine, you will never build or test the same thing twice.

natively supported Bazel

Bazel supports it.

natively supported Gradle Build Tool

Gradle Build Tool supplies a local build cache natively. Gradle Enterprise adds support for replication and management.

natively supported Lage

Lage supports it.

natively supported Lerna

Lerna v6 has built-in local computation caching powered by the Nx.

natively supported moon

moon supports it through tarballs and efficient file tree diffing while unpacking.

natively supported Nx

Like React, Nx does tree diffing when restoring the results from its cache, which, on average, makes it faster than other tools (see this benchmark comparing Nx, Lage, and Turborepo).

natively supported Pants

Pants supports it.

natively supported Rush

Rush supports it, leveraging the system tar command to restore files more quickly.

natively supported Turborepo

Turborepo supports it.
Local task orchestration

The ability to run tasks in the correct order and in parallel. All the listed tools can do it in about the same way, except Lerna, which is more limited.

natively supported Bazel

Bazel supports it.

natively supported Gradle Build Tool

Gradle Build Tool provides support for parallel tasks via configuration, and task orchestration through its flexible Groovy or Kotlin DSL.

natively supported Lage

Lage supports it.

natively supported Lerna

Lerna v6 leverages Nx to efficiently coordinate and parallelize tasks.

natively supported moon

moon supports it.

natively supported Nx

Nx supports it.

natively supported Pants

Pants supports it.

natively supported Rush

Rush supports it. Commands can be modeled either as a simple script or as separate "phases" such as build, test, etc.

natively supported Turborepo

Turborepo supports it.
Distributed computation caching

The ability to share cache artifacts across different environments. This means that your whole organisation, including CI agents, will never build or test the same thing twice.

natively supported Bazel

Bazel supports it.

natively supported Gradle Build Tool

Gradle Build Tool offers a remote distributed cache. Gradle Enterprise adds support for replication and management.

natively supported Lage

Lage supports it.

natively supported Lerna

Lerna v6 can be connected to Nx Cloud to enable distributed caching.

natively supported moon

moon supports it through moonbase.

natively supported Nx

Nx supports it.

natively supported Pants

Pants supports it.

natively supported Rush

Rush has built-in support for Azure and AWS storage, with a plugin API allowing custom cache providers.

natively supported Turborepo

Turborepo supports it.
Distributed task execution

The ability to distribute a command across many machines, while largely preserving the dev ergonomics of running it on a single machine.

natively supported Bazel

Bazel's implementation is the most sophisticated one and can scale to repos with billions of lines of code. It's also difficult to set up.

implement your own Gradle Build Tool

Gradle Enterprise can distribute Test tasks.

not supported Lage

Lage doesn't support it.

natively supported Lerna

Lerna v6 can be connected to Nx Cloud for enabling distributed task execution.

not supported moon

moon doesn't support it.

natively supported Nx

Nx's implementation isn't as sophisticated as Bazel's but it can be turned on with a small configuration change.

natively supported Pants

Pant's implementation is similar to Bazel's and uses the same Remote Execution API.

implement your own Rush

Rush provides this feature by optionally integrating with Microsoft's BuildXL accelerator.

not supported Turborepo

Turborepo doesn't support it.
Transparent remote execution

The ability to execute any command on multiple machines while developing locally.

natively supported Bazel

This is Bazel's biggest differentiator.

not supported Gradle Build Tool

Gradle Build Tool doesn't support it.

not supported Lage

Lage doesn't support it.

not supported Lerna

Lerna doesn't support it.

not supported moon

moon doesn't support it.

not supported Nx

Nx doesn't support it.

natively supported Pants

Pant's implementation is similar to Bazel's and uses the same Remote Execution API.

not supported Rush

Rush doesn't support it.

not supported Turborepo

Turborepo doesn't support it.
Detecting affected projects/packages

Determine what might be affected by a change, to run only build/test affected projects.

implement your own Bazel

Bazel doesn't have built-in support, however third-party tools such as target-determinator provide this feature by using Bazel's query language.

natively supported Gradle Build Tool

Gradle Build Tool natively provides incremental building and up-to-date detection.

natively supported Lage

Lage supports it.

natively supported Lerna

Lerna supports it.

natively supported moon

moon supports it through a combination of querying a VCS (git) and inspecting the file system.

natively supported Nx

Nx supports it. Its implementation doesn't just look at what files changed but also at the nature of the change.

natively supported Pants

Pants supports it.

natively supported Rush

The command line parameters for project selection can detect which projects are impacted by a Git diff. Rush also provides a PackageChangeAnalyzer API for automation scenarios.

natively supported Turborepo

Turborepo supports it.
Understandable
Workspace analysis

The ability to understand the project graph of the workspace without extra configuration.

implement your own Bazel

Bazel allows developers to author BUILD files. Some companies build tools that analyse workspace sources and generate the BUILD files.

natively supported Gradle Build Tool

Gradle Build Tool developers script build tasks in Groovy using build.gradle, or Kotlin using build.gradle.kts.

natively supported Lage

Lage analyses package.json files.

natively supported Lerna

Lerna analyses package.json files.

natively supported moon

moon analyses manifest files (package.json) of all tier 2 languages it supports. In the future, will support more languages with WASM plugins.

natively supported Nx

By default, Nx analyses package.json, JavaScript, and TypeScript files. It's pluggable and can be extended to support other platforms (e.g, Go, Java, Rust).

natively supported Pants

This is Pants's biggest differentiator. It uses static analysis to automatically infer file-level dependencies for all the languages and frameworks it supports.

natively supported Rush

Rush projects have the same package.json file and build scripts as a single-repo project. Tooling/configuration is shared across the monorepo by optionally creating "rig packages."

natively supported Turborepo

Turborepo analyses package.json files.
Dependency graph visualization

Visualize dependency relationships between projects and/or tasks. The visualization is interactive meaning you are able to search, filter, hide, focus/highlight & query the nodes in the graph.

natively supported Bazel

Bazel's implementation supports a custom query language to filter out node you are not interested in.

implement your own Gradle Build Tool

Gradle Build Scan provides rich dependency information, and third party tools are available for project/task graphs.

implement your own Lage

Lage doesn't come with a visualizer but it's possible to write your own.

implement your own Lerna

Lerna doesn't come with a visualizer but it's possible to write your own.

natively supported moon

moon comes with an interactive but non-filterable visualizer. Supports both dependency and project graphs.

natively supported Nx

Nx comes with an interactive visualizer that allows you to filter and explore large workspaces.

implement your own Pants

Pants doesn't come with a visualizer, but it can emit a JSON file containing the fine-grained graph structure of your codebase, which can be processed into input for visualization tools.

implement your own Rush

Rush doesn't come with a visualizer but it's possible to write your own.

natively supported Turborepo

Turborepo uses Graphviz to generate static image and HTML file of the execution plan. The implementation is not interactive.
Manageable
Source code sharing

Facilitates sharing of discrete pieces of source code.

natively supportedBazel

Bazel supports it. Any folder of files can be marked as a project and can be shared. Bazel build rules are used to enable sharing without hurting dev ergonomics.

natively supportedGradle Build Tool

Gradle Build Tool can publish shareable artifacts and consume dependencies from multiple repositories.

natively supportedLage

Lage supports it. Only npm packages can be shared.

natively supportedLerna

Lerna supports it. Only npm packages can be shared.

natively supportedmoon

moon supports it. Only packages from supported languages can be shared.

natively supportedNx

Nx supports it. Any folder of files can be marked as a project and can be shared. Nx plugins help configure WebPack, Rollup, TypeScript and other tools to enable sharing without hurting dev ergonomics.

natively supportedPants

Pants supports packaging, publishing and consuming code artifacts across repos, using the standard idioms of the underlying languages and frameworks.

natively supportedRush

Rush supports it, however discourages importing code from folders that are not a declared npm dependency. This ensures that projects can be easily moved between monorepos. For situations where creating a package is too much overhead, "packlets" offer a lightweight alternative.

natively supportedTurborepo

Turborepo supports it. Only npm packages can be shared.
Consistent tooling

The tool helps you get a consistent experience regardless of what you use to develop your projects: different JavaScript frameworks, Go, Rust, Java, etc.
In other words, the tool treats different technologies the same way.

For instance, the tool can analyze package.json and JS/TS files to figure out JS project deps, and how to build and test them. But it will analyze Cargo.toml files to do the same for Rust, or Gradle files to do the same for Java. This requires the tool to be pluggable.

natively supportedBazel

Bazel's build rules act like plugins for different technologies and frameworks.

natively supported Gradle Build Tool

Gradle Build Tool is extensible through an ecosystem of plugins allowing it to, for instance, build native with CMake or package with webpack.

not supportedLage

Lage can only run npm scripts.

not supportedLerna

Lerna can only run npm scripts.

natively supportedmoon

moon can run any binary/command that exists on the current machine. With its integrated toolchain, can also download and install necessary tools behind the scene.

natively supportedNx

Nx is pluggable. It is able to invoke npm scripts by default, but can be extended to invoke other tools (e.g., Gradle).

natively supportedPants

Pants has a robust Plugin API that provides a uniform UX across languages and frameworks. It provides multiple plugins out of the box, including for Python, Java, Scala, Go, Shell and Docker, with more on the way. You can also write your own custom build rules using the same API.

not supportedRush

Rush only builds TypeScript/JavaScript projects, recommending a decoupled approach where native components are built separately using native toolchains or BuildXL. Ideally Node.js is the only required prerequisite for monorepo developers.

not supportedTurborepo

Turborepo can only run npm scripts, but nodejs doesn’t have to be installed.
Code generation

Native support for generating code

implement your ownBazel

External generators can be used.

implement your ownGradle Build Tool

External generators can be used.

implement your ownLage

External generators can be used.

implement your ownLerna

External generators can be used.

natively supportedmoon

moon provides a file system/template based code generation layer.

natively supportedNx

Nx comes with powerful code generation capabilities. It uses a virtual file system and provides editor integration. Nx plugins provided generators for popular frameworks. Other generators can be used as well.

natively supportedPants

Pants ships with plugins for popular code generation frameworks, including Protobuf/gRPC, Thrift, Scrooge, Avro, and SOAP. There is Plugin API support for easily adding new code generators. It supports generating code in multiple languages from a single codegen source. It is able to infer dependencies by static analysis of codegen sources, and correctly invalidate generated code when those sources change.

implement your ownRush

The Rush maintainers suggest to maintain project templates as ordinary projects in the monorepo, to ensure they compile without errors. A project scaffolding command is available via a community plugin.

implement your ownTurborepo

External generators can be used.
Project constraints and visibility

Supports definition of rules to constrain dependency relationships within the repo. For instance, developers can mark some projects as private to their team so no one else can depend on them. Developers can also mark projects based on the technology used (e.g., React or Nest.js) and make sure that backend projects don't import frontend ones.

natively supportedBazel

Bazel supports visibility rules which help you separate what is private from what is public, what can be shared, etc.

implement your ownGradle Build Tool

While not supported natively, Gradle Build Tool's rich plugin possibilities allow rules like these to be developed.

implement your ownLage

A linter with a set of custom rules and extra configuration can be used to ensure that some constraints hold.

implement your ownLerna

A linter with a set of custom rules and extra configuration can be used to ensure that some constraints hold.

natively supportedmoon

moon has built-in support for project boundaries and constraints. No external tools or commands are necessary, simply tag and annotate all projects in the workspace.

natively supportedNx

Developers can annotate projects in any way they seem fit, establish invariants, and Nx will make sure they hold. It allows developers to annotate what is private and what is not, what is experimental and what is stable, etc. Nx also allows you to define public API for each package, so other developers aren't able to deep import into them.

implement your ownPants

While not yet supported natively, a custom plugin could be written to enforce such rules.

natively supportedRush

Rush can optionally require approvals when introducing new NPM dependencies (internal or external), based on project type. It also supports version policies for NPM publishing.

implement your ownTurborepo

A linter with a set of custom rules and extra configuration can be used to ensure that some constraints hold.

# A perception shift

It's complex, we know. But you're not alone in this journey.

A monorepo changes your organization

It is more than code & tools. A monorepo changes your organization & the way you think about code. By adding consistency, lowering the friction in creating new projects and performing large scale refactorings, by facilitating code sharing and cross-team collaboration, it'll allow your organization to work more efficiently.

Many solutions, for different goals

Each tool fits a specific set of needs and gives you a precise set of features.
Depending on your needs and constraints, we'll help you decide which tools best suit you.

Bazel (by Google)

A fast, scalable, multi-language and extensible build system.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
natively supported
More infoTransparent remote execution
natively supported
More infoDetecting affected projects/packages
implement your own
Understandable
More infoWorkspace analysis
implement your own
More infoDependency graph visualization
natively supported
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
natively supported
More infoCode generation
implement your own
More infoProject constraints and visibility
natively supported

Gradle (by Gradle, Inc)

A fast, flexible polyglot build system designed for multi-project builds.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
implement your own
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
implement your own
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
natively supported
More infoCode generation
implement your own
More infoProject constraints and visibility
implement your own

Lage (by Microsoft)

Task runner in JS monorepos

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
not supported
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
implement your own
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
not supported
More infoCode generation
implement your own
More infoProject constraints and visibility
implement your own

Lerna (maintained by Nrwl)

A tool for managing JavaScript projects with multiple packages.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
natively supported
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
natively supported
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
not supported
More infoCode generation
implement your own
More infoProject constraints and visibility
implement your own

moon (by moonrepo)

A task runner and monorepo management tool for the web ecosystem.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
not supported
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
natively supported
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
natively supported
More infoCode generation
natively supported
More infoProject constraints and visibility
natively supported

Nx (by Nrwl)

Next generation build system with first class monorepo support and powerful integrations.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
natively supported
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
natively supported
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
natively supported
More infoCode generation
natively supported
More infoProject constraints and visibility
natively supported

Pants (by Pants Build)

A fast, scalable, user-friendly build system for codebases of all sizes.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
natively supported
More infoTransparent remote execution
natively supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
implement your own
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
natively supported
More infoCode generation
natively supported
More infoProject constraints and visibility
implement your own

Rush (by Microsoft)

Geared for large monorepos with lots of teams and projects. Part of the Rush Stack family of projects.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
implement your own
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
implement your own
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
not supported
More infoCode generation
implement your own
More infoProject constraints and visibility
natively supported

Turborepo (by Vercel)

The high-performance build system for JavaScript & TypeScript codebases.

Fast
More infoLocal computation caching
natively supported
More infoLocal task orchestration
natively supported
More infoDistributed computation caching
natively supported
More infoDistributed task execution
not supported
More infoTransparent remote execution
not supported
More infoDetecting affected projects/packages
natively supported
Understandable
More infoWorkspace analysis
natively supported
More infoDependency graph visualization
natively supported
Manageable
More infoSource code sharing
natively supported
More infoConsistent tooling
not supported
More infoCode generation
implement your own
More infoProject constraints and visibility
implement your own

# Resources

Here is a curated list of useful videos and podcasts to go deeper or just see the information in another way.