Monorepos
Date: 2026-08-17
One repository holding many packages or applications. It makes cross-cutting changes atomic and refactoring safe; it makes CI, tooling and access control harder — and most of the pain is avoidable with task caching that most teams add too late.
A monorepo holds multiple related projects in a single version-controlled repository, with shared tooling and a single commit history.
It is not a monolith. A monorepo can contain a dozen independently-deployed services; a monolith is one deployable unit. They’re orthogonal.
The shape
repo/
├─ apps/
│ ├─ storefront/
│ ├─ admin/
│ └─ api/
├─ packages/
│ ├─ ui/ ← design system
│ ├─ analytics/
│ └─ config/
├─ package.json ← workspaces
└─ turbo.json ← task graph
What it genuinely solves
The atomic cross-cutting change — the strongest argument:
MULTI-REPO
change the design system
→ publish a version
→ PR to storefront, bump, merge
→ PR to admin, bump, merge
→ three PRs, three reviews, and a
window where they disagree
MONOREPO
one PR, one review, everything
updated together
→ CI verifies all consumers before
merge
You cannot break a consumer without seeing it fail, which is the real benefit — it converts a class of integration failure into a build failure.
Also:
- One version of everything. No diamond dependency problem
- Refactoring across boundaries is a normal operation
- Shared tooling configured once — Linting, Formatting
- Discoverability. New developers can read the whole system
What it costs
- CI runtime, unless you only run what’s affected
- Tooling complexity. Workspace resolution, task orchestration, versioning
- Repository size, which eventually affects clone and IDE indexing
- Access control. Git permissions are per-repo, so everyone can see everything
- Ownership ambiguity. Without
CODEOWNERS, nobody owns anything - Coupling by convenience. Importing across boundaries is one line, so architecture erodes unless enforced — Static Analysis
That last one is the subtle one. Multi-repo makes coupling costly and therefore deliberate; a monorepo makes it free, and the boundaries need enforcing by tooling rather than by friction.
Task orchestration is the whole game
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": { "dependsOn": ["build"] }
}
}^build means “build my dependencies first”, which lets the tool derive the correct order from the dependency graph rather than from a hand-maintained script.
Then the two things that make it fast:
AFFECTED-ONLY changed packages, and
their dependents. Skip
the rest
REMOTE CACHE a colleague's or CI's
build result restored
rather than rebuilt
— Build Caching
See: Build Caching
Without both, a monorepo’s CI is slow enough to undermine the benefit — a full build of everything on every pull request is the single most common monorepo complaint, and it’s a configuration problem rather than an inherent one.
Versioning and publishing
FIXED / LOCKED everything shares a
version
→ simple, and forces
unnecessary bumps
INDEPENDENT each package versions
separately
→ accurate, more
bookkeeping
← usually right for
published packages
NOT PUBLISHED internal packages,
consumed from source
← simplest by far
If nothing is published externally, don’t version internal packages at all. They’re always consumed at the current commit, and versioning them is ceremony — Semantic Versioning.
When to use one
GOOD FIT
a design system shared by several
apps — Design Systems
a storefront + admin + API sharing
types
one team, or several with high
overlap
frequent cross-cutting changes
POOR FIT
genuinely unrelated products
teams with no shared code
wildly different tech stacks
strict per-team access requirements
See: Design Systems
For a retail team running a storefront, an admin tool and a shared component library, the case is strong — those three change together constantly, and keeping them in step across three repositories is most of the work.