circuittimes.com
Subscribe
Explainer

Monorepos Explained: What They Are and When to Use One

What a monorepo is, how it differs from a monolith and polyrepo, and how workspaces and Turborepo work, tested for real.

Rafael Ondrejka

Updated 14 min read

Illustration of a folder with a package and a code branches

A monorepo is one repository that holds several separate projects. They live side by side, can share code and are built and tested together. The opposite approach, one repository per project, is often called a polyrepo or multi-repo setup.

This guide explains the monorepo definition, how it differs from a monolith and from microservices, how JavaScript monorepos work, and which tools help.

I also built a small one and ran it, so the outputs below are real. I checked the sources on October 23, 2026.

The short answer

On Nx's monorepo.tools site, a monorepo is defined as one repository that holds several distinct projects whose relationships are clearly defined. Putting unrelated code in one folder is not enough.

A monorepo makes shared code and cross-project changes easier. It also needs tooling, because builds and tests grow with the repository. Small teams often start with workspaces, then add a build system when it gets slow.

Good to know

A monorepo is about where code is stored. A monolith is about how software is built and deployed. They are separate choices, and you can have either, both or neither.

Monorepo meaning and definition

The same site makes a useful point. It says a repository with several projects but no defined relationships between them is just code colocation, not a monorepo. The relationships are what matter.

In practice that means projects can depend on each other in a known way, so tools can tell which projects a change affects. That is what lets a build system skip work.

A famous example

Google stores most of its code in one repository. A 2016 article in Communications of the ACM by Rachel Potvin and Josh Levenberg describes it as a monolithic repository and discusses the advantages and trade-offs of the model.

The article reports that the repository held about two billion lines of code in January 2015. Google built its own custom source control system for it, so its setup is not a template for ordinary teams.

Monorepo vs polyrepo

Polyrepo, multi-repo and many repositories all mean one repository per project. This table is our comparison. Neither approach wins in every case.

QuestionMonorepoPolyrepo
Sharing codeProjects import each other in placeShared code is usually published as a package
Cross-project changesOne change and one review can span projectsSeparate changes in each repository, kept in sync
Tooling and dependency versionsOne setup for everythingEach repository has its own
Build and test timeGrows unless the tooling skips unaffected workEach repository stays small
Access and ownershipEveryone sees one repository, so ownership rules need careAccess is easy to set per repository
Setup effortA tool such as a workspace manager or build system is almost requiredLittle extra tooling

Monorepo vs monolith

This is our explanation of two terms that sound alike. A monolith is one application deployed as a single unit. A monorepo is one repository. A monorepo can hold many independently deployed applications.

Google's article calls its repository monolithic, which describes how the code is stored. That is a different idea from a monolithic application.

Monorepo vs microservices

Microservices split a system into small services that deploy separately. They can live in one repository or in many. A monorepo does not force a monolith, and microservices do not force separate repositories.

Many teams keep several services in one monorepo so that a change to a shared contract and its users ship together. This is our analysis, not a rule.

Monorepo architecture

A typical JavaScript monorepo holds a root package.json, a folder for packages and some shared configuration. My demo uses this layout.

.
├── package.json
├── turbo.json
└── packages
    ├── app
    │   └── package.json
    └── utils
        └── package.json

The utils package has a function, and the app package imports it. The root package.json declares which folders are packages. If the app package is a front-end app, our Vite guide shows how to start one.

How workspaces work

The npm documentation describes workspaces as features for managing multiple packages from your local file system within one top-level root package. You name them in the workspaces field of the root package.json.

{
  "name": "demo-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

npm's documentation says that running npm install links the workspace folders into the root node_modules folder. I confirmed this in my demo.

$ npm install
added 2 packages in 305ms
$ ls -l node_modules/@demo
lrwxrwxrwx 1 root root 18 Oct 23 02:27 app -> ../../packages/app
lrwxrwxrwx 1 root root 20 Oct 23 02:27 utils -> ../../packages/utils

The app package can now import @demo/utils, and Node finds it through the link. There is no publishing step in between.

pnpm workspaces

pnpm describes its workspace support with a pnpm-workspace.yaml file that lists package patterns. Its documentation also covers the workspace: protocol, which makes a dependency point at a package inside the repository.

pnpm's documentation adds that since version 12.7.0, a repository with a workspaces field in package.json and no pnpm-workspace.yaml gets one created on install. Our guide to npm vs pnpm vs Yarn vs Bun compares the tools.

What went wrong in my demo

The utils package builds into a dist folder, and the app imports that built output. I ran the build in every workspace with npm. It failed.

$ npm run build --workspaces
npm notice run @demo/app@1.0.0 build
Error: Cannot find module '/tmp/mono/node_modules/@demo/utils/dist/index.js'.
Please verify that the package.json has a valid "main" entry

npm built the app first, before utils had produced its dist folder, so the import failed. Workspaces link packages together, but they do not know that one package must be built before another.

The fix is a build system

This is the gap build systems fill. Turborepo's documentation describes caching and task scheduling, and says it schedules tasks to run in parallel across available cores. It reads the scripts you already wrote in package.json.

I added a small turbo.json. The dependsOn setting with a caret tells Turborepo to build a package's dependencies first, and outputs names the folder to cache.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

First run

Turborepo built utils, then app, and both were cache misses. It took 229 milliseconds in my test.

$ npx turbo run build
Packages in scope: @demo/app, @demo/utils
@demo/utils:build: cache miss, executing 6e264f57316bc0ba
@demo/utils:build: built utils
@demo/app:build: cache miss, executing cb381314f1ec9453
@demo/app:build: built app
Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total

Second run, nothing changed

Turborepo's documentation says it restores the results of a cacheable task from a fingerprint taken the first time the task ran. Both tasks were cache hits, and the run took 5 milliseconds.

$ npx turbo run build
@demo/utils:build: cache hit, replaying logs 6e264f57316bc0ba
@demo/app:build: cache hit, replaying logs cb381314f1ec9453
Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    5ms >>> FULL TURBO

After changing the app

I added a comment to the app's source. Only the app was rebuilt, and utils came straight from the cache.

@demo/utils:build: cache hit, replaying logs 6e264f57316bc0ba
@demo/app:build: cache miss, executing 144e7731c251dc3a
Cached:    1 cached, 2 total

After changing utils

I then changed the utils package. Both tasks rebuilt, because the app depends on utils. That is the monorepo idea in miniature: change one package, and only that package and the ones that depend on it do work.

@demo/utils:build: cache miss, executing 6d3a6202712ea420
@demo/app:build: cache miss, executing 2bec5a22486df368
Cached:    0 cached, 2 total

Remote caching

Turborepo's documentation says its Remote Cache keeps task results, so your CI does not repeat work it has already done. My demo had remote caching disabled, as the run output showed.

Teams use a shared cache so a build finished on one machine can be reused on another. The documentation describes this feature, and I did not test it.

Monorepo tools

This table lists common tools and what their own documentation says. Versions are the latest on the npm registry on October 23, 2026 for the JavaScript tools.

ToolWhat its documentation says
npm, pnpm, Yarn, Bun workspacesLink multiple packages in one repository. pnpm's page notes that npm, Yarn and Bun repositories list projects in a workspaces field
Turborepo 2.11.10A build system for scaling monorepos, with caching, task scheduling and a Remote Cache
Nx 24.0.0A build system for JavaScript and TypeScript monorepos, with plugins that extend it to other languages
Lerna 10.1.1Described as the original monorepo tool for JavaScript and TypeScript, which runs a command across projects efficiently
BazelAn open-source build and test tool similar to Make, Maven and Gradle, for many languages, which caches previous work

How to choose a tool

This is our advice. If you only need packages to link together, workspaces are enough. If builds get slow or order matters, as in my demo, add a build system.

If you are mixing many languages in one repository, look at Bazel or Nx's plugins. If you publish many packages, consider Lerna. Try one on a copy of your repository first.

Benefits of a monorepo

These benefits are our summary. They match the themes in Google's article, such as code sharing and collaboration across teams.

Easier sharing

Projects use each other's code without publishing it.

Atomic changes

One change can update a library and everything that uses it.

One toolchain

Linting, testing and dependency versions are set once.

Visibility

People can see and search other teams' code.

Costs and risks

Builds grow

Without caching and affected-only runs, CI slows down as the repository grows.

Structure matters

Without defined relationships between projects, the benefits fade.

Tooling to learn

Workspaces and a build system add concepts to your team.

Access control

Everyone works in one repository, so decide ownership rules early.

When a monorepo fits

This checklist is our advice. A monorepo tends to fit when several projects share code, change together or share tooling, and the team can adopt a build tool.

Polyrepo tends to fit when projects are unrelated, owned by separate teams with different release cycles, or need different access rules. Start small: move two related packages together and see how it feels.

pnpm workspaces in practice

I also built a three-package workspace with pnpm 12.15.0 to see how it behaves. The app depends on a ui package and a utils package, and ui depends on utils. Each dependency uses the workspace protocol.

"dependencies": { "@demo/ui": "workspace:*", "@demo/utils": "workspace:*" }

After install, pnpm lists the local packages as links to their folders. Nothing is downloaded from the registry for them, so an edit to utils shows up in app immediately.

$ pnpm -r list --depth 0
@demo/app@1.0.0 /tmp/mono2/packages/app
├── @demo/ui@link:../ui
└── @demo/utils@link:../utils
@demo/ui@1.0.0 /tmp/mono2/packages/ui
└── @demo/utils@link:../utils

The order is the useful part. When I ran the build in every package with pnpm, it built utils, then ui, then app.

That is dependency order, and it avoided the failure that npm's workspaces run hit in the demo above.

$ pnpm -r run build
Scope: 3 of 4 workspace projects
built utils
built ui
built app

That is one test on a tiny repository, so do not read it as a rule for every project.

It does show why the choice of package manager matters in a monorepo. Our guide to npm vs pnpm vs Yarn vs Bun compares them.

Run only what changed

The goal of a monorepo build is to do less work, not more. Filters let you pick a package and everything it needs. In pnpm, a package name followed by three dots means that package and its dependencies.

$ pnpm --filter @demo/ui... run build
Scope: 2 of 4 workspace projects
built utils
built ui

Only the ui package and its one dependency were built. The app, which nothing here needs, was skipped. Turborepo has the same idea. In my npm demo, filtering to the utils package replayed its cache.

$ npx turbo run build --filter=@demo/utils
@demo/utils:build: cache hit, replaying logs 6d3a6202712ea420
Tasks:    1 successful, 1 total
Cached:    1 cached, 1 total

Affected-only runs

Turborepo's run reference describes an affected option that filters to the packages affected by changes on the current branch.

By default it compares your branch with main, and it can be combined with a filter, for example to build only one app when it is affected.

That comparison needs the Git history between the two points. In a continuous integration job that clones only the latest commit, it cannot see what changed, so fetch enough history for the comparison to work.

Continuous integration for a monorepo

CI is where monorepos either shine or hurt. If every change triggers every test, build times grow with the repository. These are the usual tools, and we suggest using them together.

Affected-only tasks

Build and test only the packages that the change touches.

Remote caching

Share task results between your machine and CI, so work done once is not repeated.

Parallel jobs

Split independent packages across jobs.

Start with caching and affected-only runs. They deliver most of the benefit and need little configuration.

Ownership with CODEOWNERS

When many teams share one repository, someone must be responsible for each area. GitHub's code owners feature uses a CODEOWNERS file to name the people or teams responsible for code in a repository.

The file may sit in the .github folder, the repository root or the docs folder, and GitHub uses the first one it finds.

Each file applies to one branch. People with admin or owner permission can require approval from code owners before a pull request merges.

The effect is that a change in the payments folder asks the payments team for review, even though everyone shares the repository. Check GitLab's and Bitbucket's own documentation if you use those hosts, because the details differ.

Versioning and publishing

If your packages are published to a registry, you have a choice.

One policy is a single version number for everything, which is simple but releases packages that did not change. The other is independent versions, where each package moves on its own.

Changesets is an open source tool that is described as handling versioning and changelogs, with monorepos as its focus.

Contributors add a small note about each change, and the tool turns those notes into version bumps and changelog entries. Lerna and Nx also cover releases.

If your packages are internal and only deployed together, you may not need versioning at all. Workspace links already keep them in step.

One version of each dependency

A monorepo makes it possible to use one version of each third-party library across all projects. That is our recommendation, because it avoids the situation where two apps in the same repository break in different ways on different versions.

The cost is that an upgrade touches everything at once. With good tests and affected-only runs, that is usually a fair trade, and the change lands in one pull request.

Moving an existing repository in

You can bring an old repository into a monorepo and keep its history. I tried git subtree, which adds another repository's history under a folder. The old repository had three commits.

$ git subtree add --prefix=packages/lib /tmp/oldlib main
Added dir 'packages/lib'
$ git log --oneline --graph
*   7049798 Add 'packages/lib/' from commit 'c1376d8...'
|| * c1376d8 Fix lib bug
| * 5267592 Improve lib
| * 97c39ad Add lib
* 4a39bef app change
* c735e7a baseline

The three original commits are in the graph. There is a caveat, though.

Those commits recorded files at the old repository's root, so a path-limited log for packages/lib shows only the import commit. The history is kept, but its paths are not rewritten.

Nx's documentation has a section on preserving Git histories when you import projects, so check the tool you plan to use.

Moving files into a folder and rewriting history is a bigger operation, and you should try it on a copy first.

A migration plan

This is our advice for moving several repositories into one. Go in small steps.

Pick two related projects

Start with projects that already share code, so the benefit is visible quickly.

Choose workspace tooling

Set up npm, pnpm or Yarn workspaces and confirm the build works.

Import with history

Use subtree or the importer your build tool offers, and test on a copy.

Add caching and affected-only runs

Do this before the repository grows.

Set ownership

Add a CODEOWNERS file so review stays targeted.

Retire the old repositories

Archive them and point their readmes at the new location.

When a monorepo hurts

A monorepo is not free. Google's article describes building its own custom source control system for its repository, which shows how much tooling a very large one can need. Most teams are nowhere near that size.

Still, watch for these signs: clones and checkouts that take too long, builds that run everything on every change, ownership disputes in shared folders and tools that only work in one language.

If several of these appear, consider splitting the repository.

Monorepos and Git hosting

A monorepo is just a repository to GitHub, GitLab or Bitbucket. Any repository can hold many projects. Our comparison of GitHub vs GitLab vs Bitbucket covers hosting, and our Git tutorial covers the basics.

Frequently asked questions

What exactly is a monorepo?

A single repository that holds multiple distinct projects with well-defined relationships between them.

How does a monorepo differ from a monolith?

A monorepo is about repository layout. A monolith is about how an application is built and deployed.

What is monorepo vs polyrepo?

A monorepo keeps many projects in one repository. A polyrepo keeps one project in each repository.

Do monorepos work with microservices?

Yes. Several services can share one repository and still be deployed separately.

What are the best monorepo tools?

Workspaces for linking packages, then Turborepo, Nx, Lerna or Bazel for builds, depending on your needs.

What is a monorepo in GitHub?

A repository on GitHub that contains several projects. GitHub treats it like any other repository.

How do you migrate to a monorepo?

Start with two related projects, set up workspaces, import the old repository with its history, add caching and affected-only builds, and archive the old repositories.

Do monorepos slow Git down?

A very large repository can make clones and status checks slower. Google built custom tooling for its repository. For ordinary teams, shallow clones and affected-only builds keep things manageable.

Who reviews code in a monorepo?

A CODEOWNERS file can route each folder to the team responsible for it.

A quick checklist

Up nextGitHub vs GitLab vs BitbucketOnce your code lives in one place, you choose where to host it. Read our comparison of GitHub vs GitLab vs Bitbucket.

From circuittimes.com/cloud-dev/monorepo-explained/

Was this guide helpful?

Keep going

More in Developer Tools

See all

The briefing, without the noise.

The stories that matter in technology, every weekday morning.

Free. Unsubscribe in one click.