Journal — March 19, 2026 · 5 min read
Monorepo or Not — An Honest Cost-Benefit
A ledger of what a monorepo actually buys you and what it actually costs, the threshold where it flips, and why merging repos later is far easier than splitting them.
Most monorepo arguments are identity arguments. People who like them describe a world of atomic commits and shared types; people who don't describe forty-minute CI runs and a laptop fan at full speed. Both are describing real experiences, which is why the argument never resolves.
Here's the ledger instead. Two columns, then the line where it flips.
The credit column: what a monorepo actually buys
Atomic cross-cutting changes. You rename a field in the API and update the web app, the mobile app, and three internal scripts in one commit. In a multi-repo setup that's four PRs, a merge order, a deploy sequence, and a window where things are inconsistent. This is the single largest benefit and it compounds with every service you add.
One version of every dependency. No repo running React 18 while another runs 19. No arguments about whose Zod version is right. The version conflict either doesn't exist or it's a single loud problem you fix once, rather than a slow divergence you discover during an incident.
Shared types between server and client. For a TypeScript product, this is close to decisive. The API's response type is the type the frontend consumes, checked by the compiler, with no codegen step and no drift. I've spent hours debugging a mobile bug that was one renamed enum value in a Go service and a stale hand-written TypeScript interface. That class of bug just stops existing.
One PR for a full-stack feature. Review, revert, and blame all work on the unit humans actually think in, which is the feature, not the repository.
Cheaper refactors. You can grep the entire product. Finding every call site of a function is a search, not an archaeology project across six GitHub repos with different naming conventions.
The debit column: what it actually costs
CI that must be made selective or it burns money. This is the big one. Naively, every push runs every test in every package. On a product with a web app, a mobile app, and an API, that's a twenty-minute pipeline for a CSS change. You must set up affected-package detection, remote caching, and matrix jobs before the repo gets big, not after. Budget real time for this, and read up on GitHub Actions CI/CD properly rather than copying a template.
A tooling tax that never goes away. pnpm workspaces, plus Turborepo or Nx, plus a shared tsconfig strategy, plus consistent build outputs. Someone owns this. If nobody owns it, it rots, and six months later nobody knows why the cache misses on every run.
A slower IDE. TypeScript's language server on a large workspace gets sluggish, and project references help but are fiddly to configure correctly. Editor responsiveness is a real productivity cost that never appears in any architecture doc.
Git history noise. git log is now the log of the entire company. Bisecting a mobile regression means walking past hundreds of backend commits. Path-scoped log works but nobody remembers to use it.
Coarse access control. GitHub's permission model is repo-shaped. Bringing in a contractor to work on the marketing site means giving them read access to everything, unless you fight with CODEOWNERS and rulesets, which control review rather than visibility. For agency and contract work, this genuinely matters.
Deploy coupling if you're careless. The failure mode is a deploy pipeline that ships everything on every merge to main. That's not caused by the monorepo, but the monorepo makes it the easy thing to build, and undoing it later is unpleasant.
Where the line sits
The case that decides it for most product teams is boring and clear.
A TypeScript product with a web app, a mobile app, and an API sharing types is a monorepo. The shared-types benefit alone outweighs the entire debit column, and it's part of why a full-stack framework tends to win here too, as I argued in Next.js vs React for startups. Same decision for any team where the same people cross the boundary daily.
Independent services on independent release cycles, owned by different people, are not. If the payments team ships on their own schedule and nobody outside it reads their code, a monorepo gives them CI coupling, review noise, and a tooling dependency in exchange for benefits they will never use.
The test I apply: how often does one change need to touch two of these codebases in the same week? Weekly, monorepo. A few times a year, separate repos. That question is more predictive than team size, company size, or anything about the language.
The asymmetry that should break ties
Merging two repos into a monorepo is a weekend. git subtree or git filter-repo preserves history, you write a workspace config, you fix the import paths, and it's done.
Splitting a monorepo is a project. Every shared package needs a home, a version, and a publishing pipeline. Every implicit dependency that worked because everything was on disk becomes a network call or a registry artifact. Every deploy pipeline gets rewritten. I've watched this take a quarter.
So when it's genuinely 50/50, start with separate repos and merge if the pain shows up. That's the reversible direction.
Where I got it wrong
I pushed for a monorepo on a project with an admin dashboard, a customer-facing web app, and a Go backend. My reasoning was atomic changes. What I missed was that Go and TypeScript share no tooling, so there was nothing to unify: no shared types across that boundary, no shared dependency graph, two separate build systems living in one directory pretending to be a system.
We got the costs, most visibly CI runs that rebuilt the Go binary because a Tailwind class changed, and roughly none of the benefits. Six months later the backend moved out and everyone was happier. The lesson I took: a monorepo pays off when the packages share a language and a toolchain. Across a language boundary it's mostly a directory with opinions.
The real question isn't repository layout at all. It's whether your codebases share a dependency graph or just share a company. Everything else follows from that answer, and no amount of Nx configuration will change it.