{"id":117,"date":"2026-04-18T13:01:54","date_gmt":"2026-04-18T13:01:54","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/monorepo-vs-polyrepo-when-i-pick-each\/"},"modified":"2026-04-18T13:01:54","modified_gmt":"2026-04-18T13:01:54","slug":"monorepo-vs-polyrepo-when-i-pick-each","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/monorepo-vs-polyrepo-when-i-pick-each\/","title":{"rendered":"Monorepo vs polyrepo: when I actually pick each one"},"content":{"rendered":"<p>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.<\/p>\n<p>If you&rsquo;ve been on the internet for more than fifteen minutes, you&rsquo;ve seen the monorepo vs polyrepo fight. It&rsquo;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&rsquo;m less certain and more opinionated, which I think is generally a good trade.<\/p>\n<p>I&rsquo;ve shipped both. I&rsquo;ve shipped a two-service Laravel plus Next.js project that should have been one repo and was three. I&rsquo;ve shipped a monorepo with six apps and a shared UI package that was great until CI started taking twenty minutes. So here&rsquo;s what I actually think, with fewer slogans and more scars.<\/p>\n<h2 id=\"what-people-mean-when-they-say-monorepo-or-polyrepo\">what people mean when they say &ldquo;monorepo&rdquo; or &ldquo;polyrepo&rdquo;<\/h2>\n<p>A monorepo is a single version-controlled repository that contains multiple projects. Not a giant <code>app\/<\/code> folder with everything jammed together. Usually it looks something like:<\/p>\n<pre><code>my-repo\/\n  apps\/\n    web\/            # Next.js\n    admin\/          # Next.js\n    api\/            # Laravel or Go\n  packages\/\n    ui\/             # shared React components\n    config\/         # eslint, tsconfig, tailwind preset\n    sdk\/            # typed API client\n  turbo.json\n  package.json\n<\/code><\/pre>\n<p>A polyrepo (sometimes &ldquo;multi-repo&rdquo;) is the opposite. Each of those folders lives in its own repo, with its own CI, its own releases, and its own <code>README.md<\/code> that nobody reads.<\/p>\n<p>So the real question isn&rsquo;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.<\/p>\n<h2 id=\"the-case-for-a-monorepo-thats-honest-about-costs\">the case for a monorepo that&rsquo;s honest about costs<\/h2>\n<p>The <a href=\"https:\/\/research.google\/pubs\/why-google-stores-billions-of-lines-of-code-in-a-single-repository\/\" rel=\"nofollow noopener\" target=\"_blank\">Google engineering paper on their giant monorepo<\/a> is the famous example. Dan Luu wrote a solid <a href=\"https:\/\/danluu.com\/monorepo\/\" rel=\"nofollow noopener\" target=\"_blank\">summary of the tradeoffs<\/a> back when I was still in university. The short version of the pro-monorepo argument is:<\/p>\n<ol>\n<li>Atomic cross-project changes. You rename a function in <code>sdk\/<\/code>, you fix every call site in one commit. No coordinated merges across four repos.<\/li>\n<li>One dependency graph. <code>pnpm-workspace<\/code> or Go workspaces or Cargo workspaces give you one lockfile, one toolchain version, one source of truth about which library is pinned.<\/li>\n<li>Refactor bravery. When changes are cheap, you make them. When they cost a three-way PR dance, you don&rsquo;t, and the codebase slowly rots.<\/li>\n<li>Easier internal code reuse. A shared UI package, a shared tracing library, a shared API client. Everyone picks the same one because there&rsquo;s only one to pick.<\/li>\n<\/ol>\n<p>That is all real. I&rsquo;ve felt all of it. When I moved my React shadcn components into <code>packages\/ui<\/code> and suddenly both apps used the same <code>&lt;Button&gt;<\/code>, I almost cried.<\/p>\n<p>But the part people skip is the cost. In a real monorepo you pay for:<\/p>\n<ul>\n<li>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 <code>apps\/admin<\/code> broke nothing.<\/li>\n<li>Build tooling that&rsquo;s more than <code>tsc<\/code>. You end up adding <a href=\"https:\/\/turbo.build\/repo\/docs\" rel=\"nofollow noopener\" target=\"_blank\">Turborepo<\/a> or Nx or Bazel, and now you&rsquo;re also maintaining that thing too.<\/li>\n<li>Access control. Everyone can read everything. Sometimes that&rsquo;s fine. Sometimes a contractor for the marketing site should not be inside the billing code.<\/li>\n<li>A coupled deploy graph that has to be untangled by hand. Just because everything lives together doesn&rsquo;t mean everything ships together, and pretending it does is how outages happen.<\/li>\n<\/ul>\n<p>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&rsquo;re waiting on <code>pnpm install<\/code> for three minutes on every PR, it hurts a lot.<\/p>\n<h2 id=\"the-case-for-polyrepo-and-why-im-not-a-purist-about-it\">the case for polyrepo, and why I&rsquo;m not a purist about it<\/h2>\n<p>The strongest rebuttal I&rsquo;ve read is Matt Klein&rsquo;s <a href=\"https:\/\/medium.com\/@mattklein123\/monorepos-please-dont-e9a279be011b\" rel=\"nofollow noopener\" target=\"_blank\">Monorepos: Please don&rsquo;t<\/a>. He was at Lyft when they made the opposite decision from Google. His argument, roughly, is that small teams copying Google&rsquo;s tooling without Google&rsquo;s tooling investment is a trap. I&rsquo;ve watched teams adopt Nx because they read a blog post, then spend weeks fighting cache keys instead of shipping features.<\/p>\n<p>Polyrepo wins on a few specific things:<\/p>\n<ul>\n<li>You inherit tiny, obvious CI. One repo, one pipeline, one deploy. You can explain the whole system in a paragraph.<\/li>\n<li>Clearer ownership. Repo equals team. It&rsquo;s a social contract that prevents random people from drive-by-editing your code.<\/li>\n<li>No &ldquo;everything is coupled to everything&rdquo; vibes. Different services can pin different versions of a library, or use entirely different languages, without ceremony.<\/li>\n<li>Release velocity per-service. Deploys happen when the service is ready, not when the whole graph is green.<\/li>\n<\/ul>\n<p>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.<\/p>\n<p>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&rsquo;d rather pay, and then you pay it.<\/p>\n<h2 id=\"the-boring-framework-i-actually-use-now\">the boring framework I actually use now<\/h2>\n<p>I used to think this was a structural question. It&rsquo;s not. It&rsquo;s a people question. Here&rsquo;s what I ask first, roughly in order:<\/p>\n<ul>\n<li>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.<\/li>\n<li>How often do I want to make a change that touches both frontend and backend in the same PR? If it&rsquo;s daily, monorepo wins. If it&rsquo;s monthly, the coordination cost of a polyrepo is fine.<\/li>\n<li>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.<\/li>\n<li>Do I have someone who can own build and CI for the monorepo? Not in a hero-coder way. In a &ldquo;this is part of their job&rdquo; way. If no, don&rsquo;t adopt one.<\/li>\n<li>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.<\/li>\n<\/ul>\n<p>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 <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-server-actions-stopped-writing-api-routes\/\" rel=\"noopener\">how Next.js server actions finally got me off API routes<\/a>, and server actions also collapse the &ldquo;where does this function live&rdquo; question. That reshapes whether you even need a shared SDK package.<\/p>\n<h2 id=\"tooling-matters-more-than-people-who-love-tooling-will-admit\">tooling matters (more than people who love tooling will admit)<\/h2>\n<p>Here is the part I got wrong for a long time. I thought &ldquo;monorepo vs polyrepo&rdquo; was mostly a code organization question. It is not. It is a build system question dressed up as a code organization question.<\/p>\n<p>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&rsquo;t invest in caching and task pipelines. The inflection point between &ldquo;polyrepo with some glue&rdquo; and &ldquo;monorepo with Turborepo&rdquo; 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.<\/p>\n<p>If you&rsquo;re going monorepo, my opinionated suggestion is to start simple. pnpm workspaces plus Turborepo plus GitHub Actions with a good <code>paths<\/code> filter gets you 80% of the benefit for maybe 20% of the pain. Don&rsquo;t reach for Nx or Bazel until you&rsquo;re literally waiting on builds.<\/p>\n<p>A minimal <code>turbo.json<\/code> that I actually use looks like this:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;$schema&quot;: &quot;https:\/\/turbo.build\/schema.json&quot;,\n  &quot;globalDependencies&quot;: [&quot;**\/.env&quot;],\n  &quot;tasks&quot;: {\n    &quot;build&quot;: {\n      &quot;dependsOn&quot;: [&quot;^build&quot;],\n      &quot;outputs&quot;: [&quot;.next\/**&quot;, &quot;!.next\/cache\/**&quot;, &quot;dist\/**&quot;]\n    },\n    &quot;lint&quot;: { &quot;dependsOn&quot;: [&quot;^build&quot;] },\n    &quot;test&quot;: { &quot;dependsOn&quot;: [&quot;^build&quot;], &quot;cache&quot;: true },\n    &quot;dev&quot;:  { &quot;cache&quot;: false, &quot;persistent&quot;: true }\n  }\n}\n<\/code><\/pre>\n<p>That&rsquo;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.<\/p>\n<p>If you&rsquo;re going polyrepo, pick a standard repo template (a cookiecutter for Python, a <code>create-<\/code> 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.<\/p>\n<h2 id=\"what-i-would-actually-do-if-i-were-starting-a-new-project-today\">what I would actually do if I were starting a new project today<\/h2>\n<p>Alright, here is my honest answer, with the caveat that it might be different six months from now.<\/p>\n<p>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 <code>apps\/web<\/code> folder and an <code>apps\/api<\/code> folder. No workspaces, no Turborepo, just two apps that happen to live in the same folder. This is the most underrated setup on earth.<\/p>\n<p>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 <code>packages\/<\/code>. No one-off utilities, no &ldquo;just parking this here&rdquo; folders.<\/p>\n<p>For a team bigger than that, with multiple languages and multiple release cadences: polyrepo per-service, with a template repo and a shared &ldquo;platform&rdquo; repo for things like base Docker images and shared CI snippets.<\/p>\n<p>And if you want to see how I actually lay this out on real projects, I keep examples and case studies on <a href=\"https:\/\/abrarqasim.com\/about\" rel=\"noopener\">my portfolio<\/a> that are worth a look before you copy anyone else&rsquo;s layout.<\/p>\n<h2 id=\"one-concrete-thing-you-can-do-this-week\">one concrete thing you can do this week<\/h2>\n<p>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&rsquo;t need one now, no matter how good the Nx docs look. That&rsquo;s the whole test.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve shipped both monorepo and polyrepo setups. Here are the specific team traits and constraints that actually decide which one wins for your codebase.<\/p>\n","protected":false},"author":2,"featured_media":116,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I've shipped both monorepo and polyrepo setups. Here are the specific team traits and constraints that actually decide which one wins for your codebase.","rank_math_focus_keyword":"monorepo vs polyrepo","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[82,80,77,78,79,81],"class_list":["post-117","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-developer-productivity","tag-devops","tag-monorepo","tag-polyrepo","tag-repo-structure","tag-turborepo"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/117","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=117"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/117\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/116"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=117"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=117"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=117"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}