Okay, this is going to sound dumb, but I spent two weeks last year arguing with a co-founder about whether to move four small services into a single repo. I lost. We moved them in. Six months later, we moved them back out. Neither of us has ever fully recovered.
If you’ve been on the internet for more than fifteen minutes, you’ve seen the monorepo vs polyrepo fight. It’s one of those debates where everyone is a little bit right and also extremely certain about it. I used to be certain too. Now I’m less certain and more opinionated, which I think is generally a good trade.
I’ve shipped both. I’ve shipped a two-service Laravel plus Next.js project that should have been one repo and was three. I’ve shipped a monorepo with six apps and a shared UI package that was great until CI started taking twenty minutes. So here’s what I actually think, with fewer slogans and more scars.
what people mean when they say “monorepo” or “polyrepo”
A monorepo is a single version-controlled repository that contains multiple projects. Not a giant app/ folder with everything jammed together. Usually it looks something like:
my-repo/
apps/
web/ # Next.js
admin/ # Next.js
api/ # Laravel or Go
packages/
ui/ # shared React components
config/ # eslint, tsconfig, tailwind preset
sdk/ # typed API client
turbo.json
package.json
A polyrepo (sometimes “multi-repo”) is the opposite. Each of those folders lives in its own repo, with its own CI, its own releases, and its own README.md that nobody reads.
So the real question isn’t one repo or many. The real question is what lives next to what, and how do changes ripple through your system when you touch one piece.
the case for a monorepo that’s honest about costs
The Google engineering paper on their giant monorepo is the famous example. Dan Luu wrote a solid summary of the tradeoffs back when I was still in university. The short version of the pro-monorepo argument is:
- Atomic cross-project changes. You rename a function in
sdk/, you fix every call site in one commit. No coordinated merges across four repos. - One dependency graph.
pnpm-workspaceor Go workspaces or Cargo workspaces give you one lockfile, one toolchain version, one source of truth about which library is pinned. - Refactor bravery. When changes are cheap, you make them. When they cost a three-way PR dance, you don’t, and the codebase slowly rots.
- Easier internal code reuse. A shared UI package, a shared tracing library, a shared API client. Everyone picks the same one because there’s only one to pick.
That is all real. I’ve felt all of it. When I moved my React shadcn components into packages/ui and suddenly both apps used the same <Button>, I almost cried.
But the part people skip is the cost. In a real monorepo you pay for:
- CI that has to be smart enough to only run tests for things that actually changed. Otherwise you wait ten minutes to find out a typo in
apps/adminbroke nothing. - Build tooling that’s more than
tsc. You end up adding Turborepo or Nx or Bazel, and now you’re also maintaining that thing too. - Access control. Everyone can read everything. Sometimes that’s fine. Sometimes a contractor for the marketing site should not be inside the billing code.
- A coupled deploy graph that has to be untangled by hand. Just because everything lives together doesn’t mean everything ships together, and pretending it does is how outages happen.
If your team is four people and your CI times are under five minutes, none of this hurts yet. If your team is forty and you’re waiting on pnpm install for three minutes on every PR, it hurts a lot.
the case for polyrepo, and why I’m not a purist about it
The strongest rebuttal I’ve read is Matt Klein’s Monorepos: Please don’t. He was at Lyft when they made the opposite decision from Google. His argument, roughly, is that small teams copying Google’s tooling without Google’s tooling investment is a trap. I’ve watched teams adopt Nx because they read a blog post, then spend weeks fighting cache keys instead of shipping features.
Polyrepo wins on a few specific things:
- You inherit tiny, obvious CI. One repo, one pipeline, one deploy. You can explain the whole system in a paragraph.
- Clearer ownership. Repo equals team. It’s a social contract that prevents random people from drive-by-editing your code.
- No “everything is coupled to everything” vibes. Different services can pin different versions of a library, or use entirely different languages, without ceremony.
- Release velocity per-service. Deploys happen when the service is ready, not when the whole graph is green.
The price is coordination. If your frontend depends on a typed API client, and that client lives in the backend repo, and you also want to version it properly, you now have a publishing step. Congratulations, you are running a miniature npm registry.
Most teams I see default to polyrepo, get tired of the coordination tax, then overcorrect into a monorepo and discover the CI tax is worse. Both taxes are real. You pick which one you’d rather pay, and then you pay it.
the boring framework I actually use now
I used to think this was a structural question. It’s not. It’s a people question. Here’s what I ask first, roughly in order:
- How many projects actually share code today, and will they in six months? If the answer is zero or one, polyrepo. You are pretending to need a monorepo.
- How often do I want to make a change that touches both frontend and backend in the same PR? If it’s daily, monorepo wins. If it’s monthly, the coordination cost of a polyrepo is fine.
- How big is the team, and how varied is their tooling? A five-person team all writing TypeScript gets most of the monorepo benefits for free. A twenty-person team with Go, PHP, and TypeScript services is fighting their build system forever.
- Do I have someone who can own build and CI for the monorepo? Not in a hero-coder way. In a “this is part of their job” way. If no, don’t adopt one.
- Are there regulated or auditable components? Billing code, secrets, customer PII. A separate repo with its own access rules is sometimes the right answer for reasons that have nothing to do with velocity.
For most of the small-to-medium projects I work on, the answer ends up being one repo per product-bounded app, plus a small monorepo if there is genuinely shared TypeScript code. I covered a related angle in how Next.js server actions finally got me off API routes, and server actions also collapse the “where does this function live” question. That reshapes whether you even need a shared SDK package.
tooling matters (more than people who love tooling will admit)
Here is the part I got wrong for a long time. I thought “monorepo vs polyrepo” was mostly a code organization question. It is not. It is a build system question dressed up as a code organization question.
A polyrepo is easy to set up and scales badly as shared code grows. A monorepo is harder to set up and scales badly if you don’t invest in caching and task pipelines. The inflection point between “polyrepo with some glue” and “monorepo with Turborepo” is where most teams actually live, and most of them pick wrong because picking right requires knowing what your CI bill will look like in a year.
If you’re going monorepo, my opinionated suggestion is to start simple. pnpm workspaces plus Turborepo plus GitHub Actions with a good paths filter gets you 80% of the benefit for maybe 20% of the pain. Don’t reach for Nx or Bazel until you’re literally waiting on builds.
A minimal turbo.json that I actually use looks like this:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": { "dependsOn": ["^build"] },
"test": { "dependsOn": ["^build"], "cache": true },
"dev": { "cache": false, "persistent": true }
}
}
That’s it. No custom executors, no graph plugins, no extra config language. When you add your third shared package and this starts creaking, then reach for more.
If you’re going polyrepo, pick a standard repo template (a cookiecutter for Python, a create- template for TypeScript, whatever) and use it every single time. The thing that makes polyrepos feel chaotic is that every repo invents its own folder structure and CI conventions.
what I would actually do if I were starting a new project today
Alright, here is my honest answer, with the caveat that it might be different six months from now.
For a solo project or a team of two shipping a web app and an API: one repo. Not a monorepo. Just a single repo with an apps/web folder and an apps/api folder. No workspaces, no Turborepo, just two apps that happen to live in the same folder. This is the most underrated setup on earth.
For a team of three to ten, shipping multiple products that share at least one UI package or SDK: pnpm workspaces monorepo with Turborepo. Strict about what goes in packages/. No one-off utilities, no “just parking this here” folders.
For a team bigger than that, with multiple languages and multiple release cadences: polyrepo per-service, with a template repo and a shared “platform” repo for things like base Docker images and shared CI snippets.
And if you want to see how I actually lay this out on real projects, I keep examples and case studies on my portfolio that are worth a look before you copy anyone else’s layout.
one concrete thing you can do this week
Open your current setup. For each repo, ask: if I deleted this repo tomorrow, how many other repos would break? If the answer is more than two, you probably should have had a monorepo yesterday. If the answer is none, you probably don’t need one now, no matter how good the Nx docs look. That’s the whole test.