What monorepo, polyrepo and hybrid actually mean
A monorepo is a single version-controlled repository that holds many independently meaningful projects, such as services, libraries, infrastructure definitions and tooling. It is not a monolith. A monolith is one deployable artifact. A monorepo can hold fifty services that build, test and deploy on their own schedules. The repository sets where the code lives and how changes are recorded. It does not set how the software runs.
A polyrepo setup gives each service or library its own repository, with its own history, permissions, CI pipeline and release cadence. Repositories share code by publishing versioned artifacts, such as packages, container images or generated API clients, and consuming them as declared dependencies.
Hybrids are common and legitimate. Typical forms include a handful of domain repositories (payments, identity, data platform), a shared-libraries repo next to per-service repos, or one monorepo for most code with a few repositories split out for security or licensing reasons.
The core thesis of this article is that repository strategy decides where you pay coordination costs, not whether you pay them. A monorepo pays upfront: build tooling that understands the dependency graph, CI that tests only what changed, and ownership rules at directory level. A polyrepo pays later and in smaller pieces: version bumps, dependency drift between consumers, and changes that need coordinated pull requests across several repositories.
Teams choose monorepos for atomic cross-project changes and a single source of truth. They choose polyrepos for strong isolation, independent autonomy and simpler per-repo tooling.
MONOREPO (one repo; not a monolith: each project deploys on its own)
+----------------------------------------------------------------+
| repo: platform |
| +--------+ +--------+ +--------+ +--------------+ |
| | svc-a | | svc-b | | lib-x | | infra, tools | |
| +--------+ +--------+ +--------+ +--------------+ |
| graph-aware build | affected-only CI | directory owners |
+----------------------------------------------------------------+
pays UPFRONT in build/CI tooling --> gains atomic cross-project changes
POLYREPO (one repo per service/library; share via versioned artifacts)
+---------+ publish +------------+ v1 +---------+
| repo: | ----------> | registry: | --------> | repo: |
| lib-x | v2 | packages, | | svc-a |
+---------+ | images, | v2 +---------+
| API clients| --------> | repo: |
+------------+ | svc-b |
+---------+
pays LATER: version bumps, drift (svc-a on v1, svc-b on v2), multi-repo PRs
HYBRID (a few domain repos, or a monorepo with a few repos split out)
+------------+ +------------+ +---------------+ +-------------+
| payments | | identity | | data-platform | | shared-libs |
+------------+ +------------+ +---------------+ +-------------+
All three repository strategies pay for coordination: a monorepo pays upfront in build and CI tooling, a polyrepo pays later in versioned artifacts, dependency drift and cross-repo changes, and a hybrid splits code along domain lines.
How code sharing and cross-boundary changes work in each model
In a monorepo, shared code is consumed at HEAD. A service depends on a library by path, for example "auth-client": "workspace:*" in a JavaScript workspace, so every build compiles against whatever the library looks like right now. When a library owner changes a function signature, they update the library and every caller in one atomic commit, reviewed once and tested together. The cost falls on the owner. A breaking change cannot merge until every caller is fixed, so owners of widely used libraries spend real time editing code they don't own. That pressure keeps APIs small and stable.
In a polyrepo, shared code is published as a versioned package, and each consumer pins a release such as "auth-client": "2.3.1". Consumers upgrade on their own schedule, which isolates teams from each other's changes but brings several problems:
- Drift: services stay pinned to old versions for months because upgrading is never urgent.
- Security patches: a fix in a shared library means bumping, testing and deploying each repository separately.
- Diamond dependencies: two libraries require incompatible versions of a third, and the consumer has to resolve the conflict.
- Ordered releases: a cross-cutting change ships library first, then consumers, across several repositories with no atomic rollback.
- Compatibility shims: owners keep old code paths alive until the last consumer migrates.
# --- Monorepo: pnpm-workspace.yaml at the repo root ---
# Every package under libs/ and services/ is part of one workspace.
packages:
- "libs/*"
- "services/*"
# --- Monorepo: services/checkout/package.json ---
# "workspace:*" links libs/auth-client from this repo, so checkout always
# builds against the library at HEAD. A signature change in auth-client
# breaks this build in the same commit, so the owner must fix it here.
{
"name": "checkout",
"private": true,
"dependencies": {
"auth-client": "workspace:*"
}
}
# --- Polyrepo: package.json in the separate checkout repository ---
# An exact pin to a published release. checkout gets nothing new, including
# security fixes, until someone opens a PR in this repo to bump the version.
{
"name": "checkout",
"private": true,
"dependencies": {
"auth-client": "2.3.1"
}
}
The same service depends on a shared auth-client library in two ways: through a pnpm workspace path link that always resolves to HEAD in a monorepo, and through an exact version pin in a polyrepo that changes only when the consumer upgrades.
Deployment, ownership and access control
None of that coordination cost comes from deployment. A common myth is that one repository means one release. It doesn't: each service in a monorepo can keep its own pipeline, version and release schedule. The difference is how the build system decides what to ship. A commit changes files, not services, so the build has to map changed paths to every target that depends on them, then rebuild, test and deploy only those artifacts. If the dependency graph is incomplete, the build either rebuilds everything or misses a consumer that should have been redeployed.
Ownership and review
In a monorepo, ownership is defined by path. A CODEOWNERS-style file maps directories to teams. For example, /services/billing/ maps to the payments team, /libs/auth/ to platform security and /infra/ to SRE. A change under a path needs approval from that path's owners, so a single repository can still enforce several review boundaries. The catch is that these rules drift as directories move. Read access is also usually granted to the whole repository.
A polyrepo gives you these boundaries by default. Repository permissions control who can read, write and merge. This matters when some code has to stay restricted.
Coupling and drift
Each model has its own failure mode. In a monorepo, importing another team's internal package takes one line, so private code can quietly become a shared API. Visibility rules in the build system, or lint checks on module boundaries, keep internal code internal. In a polyrepo, each team picks its own CI, linters and language versions. Standards then drift apart until a shared template or policy brings them back in line.
# CODEOWNERS-style path ownership for a monorepo.
# Each line maps a path pattern to the owners whose approval is required.
# Later matches take precedence, so the broad default comes first
# and more specific paths follow.
# Fallback: anything not matched below needs an architecture reviewer.
* @example-org/architecture
# Service code: each team owns its own deployable unit.
/services/billing/ @example-org/payments
/services/search/ @example-org/search
# Shared libraries: changes affect many consumers, so the owning
# team reviews every edit.
/libs/auth/ @example-org/platform-security
/libs/logging/ @example-org/platform
# Infrastructure and deployment config belong to SRE.
/infra/ @example-org/sre
# Build and CI config decides which artifacts get rebuilt and shipped,
# so a mistake here can skip or over-deploy services.
/.ci/ @example-org/platform @example-org/sre
# Database migrations for billing need both the service team and DBAs.
/services/billing/migrations/ @example-org/payments @example-org/dba
# Keep the ownership file itself under platform review so rules
# don't silently drift when directories move.
/CODEOWNERS @example-org/platform
A CODEOWNERS-style file that enforces separate review boundaries for services, shared libraries, infrastructure and build config inside a single repository, with later, more specific patterns overriding the broad default.
Tooling cost and its impact on engineering time
…exists, and that template becomes a project of its own. Both models spend engineering time on tooling. The difference is where the cost appears and how it grows.
In a monorepo, a CI pipeline that builds and tests everything on every change eventually stops scaling. The fix is a build system that understands the dependency graph, detects affected targets, and caches results so unchanged code is not rebuilt. Git operations and IDE indexing also slow down as history and file count grow. Partial clones (git clone --filter=blob:none) and sparse checkouts (git sparse-checkout set <dirs>) help, but every developer has to learn them. Flaky tests are a bigger problem here: one team's broken test can block unrelated merges. All of this needs a named owner, usually a platform team.
In a polyrepo, the cost is spread across repositories. Each one needs a CI template, dependency update automation, release and versioning conventions, and configuration that matches the others. Each item is cheap on its own, but the total grows with the number of repositories.
| Cost driver | Monorepo | Polyrepo |
|---|---|---|
| CI scaling | Graph-aware builds, caching | Per-repo templates |
| Git/IDE performance | Partial clone, sparse checkout | Rarely an issue |
| Failure blast radius | Shared; flaky tests block others | Isolated per repo |
| Growth pattern | Step cost, then mostly fixed | Linear with repo count |
Illustrative estimate (assumed numbers, not a benchmark). Polyrepo upkeep = repos × hours per repo per month. With 60 repos at 1.5 hours each, that is 90 hours per month. Compare that with an assumed monorepo build team of 0.5 FTE at about 160 hours per month, which is 80 hours. Below roughly 53 repos (80 ÷ 1.5), polyrepo upkeep costs less.
#!/usr/bin/env bash
# Clone only what one team needs from a large monorepo.
set -euo pipefail
REPO_URL="git@git.example.internal:acme/monorepo.git" # placeholder
# Partial clone: fetch commits and trees now, file contents (blobs) on demand.
# --sparse starts with only top-level files checked out.
git clone --filter=blob:none --sparse "$REPO_URL" monorepo
cd monorepo
# Check out only the directories this team works on (cone mode).
git sparse-checkout set services/payments libs/common
# Add another directory later without re-cloning.
git sparse-checkout add libs/observability
# Show which directories are currently checked out.
git sparse-checkout list
# Simple affected-path check before running CI locally:
# list files changed on this branch relative to main.
git fetch origin main
git diff --name-only origin/main...HEAD
# Restore a full working tree if needed.
# git sparse-checkout disable
A partial clone combined with a sparse checkout limits a developer's monorepo working tree to the directories their team owns, with a plain git diff showing which paths a branch changed.
A decision framework: scoring your organization
the polyrepo overhead is cheaper; above it, the dedicated build team pays for itself. That break-even is one input. Score each factor below from 0 (favours polyrepo) to 2 (favours monorepo) and add the results.
| Factor | 0 points | 1 point | 2 points |
|---|---|---|---|
| Cross-boundary change frequency | Rare; services change independently | Monthly coordinated changes | Weekly or more |
| Shared library churn | Stable, versioned rarely | Moderate | Frequent breaking changes |
| Platform and tooling capacity | No dedicated owners | Part-time ownership | Funded build team |
| Language and toolchain heterogeneity | Many stacks, divergent builds | Two or three stacks | One dominant stack |
| Team isolation or compliance needs | Hard boundaries required | Some restricted code | Broad read access acceptable |
The last two factors are inverted on purpose. Heterogeneity raises the cost of a unified build graph. Strict isolation requirements fight the default openness of a single repository.
Use these thresholds as a starting heuristic, not a rule: 0–3 points a polyrepo, 4–6 a hybrid, 7–10 a monorepo.
Three common profiles:
- Single-stack product company with a platform team (shared SDK revised weekly): scores around 9. A monorepo turns version-skew coordination into atomic commits.
- Acquisition-driven organization with Java, Go and legacy .NET estates and no central build owners: scores around 2. Forcing one repository would mean building tooling nobody is funded to maintain.
- Regulated firm with a fast-moving core platform but isolated payment or health-data services: scores around 5. A hybrid fits best, with the platform in a monorepo and audited services in separate repositories with their own access controls.
Score each part of the organization separately. A single company-wide score often hides this split.
When to avoid each approach, and when a hybrid is the better alternative
Signals that a monorepo is a poor fit
The clearest warning sign is having no one to own the build system. A monorepo only stays fast if someone maintains incremental builds, affected-target detection and CI caching. Without that work, every commit triggers more work than it needs to, and the cost grows with the size of the repository rather than the size of the change.
Highly heterogeneous toolchains are a second signal. A repository that mixes several languages, each with its own package manager and build conventions, makes a unified build graph expensive to maintain. Teams then work around the shared tooling instead of using it.
Strict isolation requirements are the third. If regulation or contracts require that certain engineers cannot read certain code, path-level permissions inside one repository are harder to audit than separate repositories.
Signals that a polyrepo is a poor fit
Watch how often a single feature needs coordinated pull requests across several repositories. Each one adds version bumps, release ordering and a window where services disagree. High churn in shared libraries is the same problem in another form: consumers fall behind, and the organization ends up supporting many versions at once.
Hybrids, and how they fail
A hybrid is usually the practical alternative. It only works when its boundaries follow real change patterns, such as code that is routinely modified together. If the boundaries come from org charts or arbitrary groupings, you pay for monorepo tooling and still coordinate changes across repositories, which gives you the downsides of both models.
How to migrate between models incrementally
Whichever direction you move, migrate one domain at a time. A single cutover forces every team to change workflows on the same day and leaves no working baseline to fall back to. Moving one bounded context at a time lets you fix tooling gaps while the blast radius is still small.
Preserving history
History matters because git blame and git log are how engineers recover the reasoning behind old decisions. To consolidate, move the source repository's files into the target subdirectory in a commit on that repository. Then merge it into the monorepo with git merge --allow-unrelated-histories. git subtree add --prefix=<dir> does both steps in one command. To split a directory out, git subtree split --prefix=<dir> produces a branch that contains only that directory's history, which you can push to a new repository.
Rewiring CI and dependencies
Moving code also changes how CI and dependencies work. Going into a monorepo, each imported service needs path-filtered triggers so a change to one service does not rebuild everything. Internal consumers also switch from pinned package versions to path references, so they now build against the current source instead of a release. Going the other way, you must add a publishing pipeline and a versioning policy before you remove any path reference. Otherwise consumers have nothing to pin to.
Cost and inertia
Expect the migration to take engineering time away from feature work, and expect resistance from teams whose current workflow already works for them. Choose the first domains to migrate based on where the current model causes the most pain, so the benefit is visible early.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to monorepo vs polyrepo, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.