All posts
Engineering May 28, 2026 8 min read

Why we run on monorepos again

After three years of polyrepos, the cost of context switching outgrew the cost of one big lockfile. Here is what we kept, what we changed, and the tooling that finally made it work.

A

Anders Lindqvist

Principal Engineer

In 2023 we split everything. Twelve services, twelve repositories, twelve pipelines — the textbook picture of team autonomy. By late 2025 we were spending our Fridays writing version-bump PRs and our Mondays debugging which combination of those bumps had broken staging. This is the story of going back, and why it stuck this time.

What polyrepos actually cost us

The pitch for polyrepos is isolation: small blast radius, independent deploys, clear ownership. All true. What the pitch leaves out is that isolation is also a tax on every change that crosses a boundary — and in a product company, the changes that matter almost always cross a boundary.

  • A shared type change touched five repos, five PRs, five reviews — and landed over three days, in the wrong order, twice.
  • Refactoring became archaeology: git blame stopped at the repo wall, so nobody knew why the contract looked the way it did.
  • Each repo grew its own lint config, CI quirks, and Node version. “It works in checkout-service” became a sentence we said out loud.

We optimized for the org chart we wanted and got the integration costs we deserved.

Sprint retro, November 2025

The three tools that changed the math

Monorepos failed for us in 2022 because every commit rebuilt the world. The tooling that exists now makes the big repo behave like many small ones — without the walls. Three pieces did the heavy lifting: task-graph caching, affected-only CI, and workspace constraints that encode the dependency rules we used to enforce with repository boundaries.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "test/**"]
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"],
      "cache": false
    }
  }
}

With remote caching on, a one-line change in the docs package builds in eleven seconds. The full cold build is still four minutes — we just stopped paying it on every push. CI runs turbo run test --filter=...[origin/main] and touches only what the diff touches.

What we kept from the polyrepo years

Going back to a monorepo did not mean going back to a monolith. Service boundaries survived; only the repository walls came down. Ownership lives in CODEOWNERS instead of separate remotes, and deploys stayed independent — the task graph knows which services a commit reaches, and only those ship.

The one rule we never break

Trunk stays green. A red main in a monorepo blocks forty people, not four — so merge queues and affected-only checks are not optional extras, they are the deal you sign when you co-locate the code.

The verdict, six months in

Cross-cutting changes went from days to hours. Onboarding went from “clone these nine repos and pray” to git clone && pnpm i. And the Friday version-bump ritual is gone — there are no versions between our own packages anymore, only the current commit. The lockfile is enormous. We have made peace with that.

A

Anders Lindqvist

Principal Engineer

Builds platforms and the teams that run them. Twenty years of shipping, ten of un-shipping other people’s microservices.