I often find myself caught up in debates about repository structure. The argument is usually the same: just use a monorepo. It makes sharing packages easier, you can handle cross-module changes in one go, and the tooling stays uniform. It sounds ideal on paper, but in practice, monorepos have their downsides. You eventually run into bloated CI times, confusion over who owns which module, and workspace structures that are just plain messy.
To be honest, I don't even consider a monorepo unless the process of sharing code actually starts to hurt. I won't adopt one just because I "hope" we'll need it later.
The real question isn't "should everything be in one repo?" but rather: what is our actual unit of change?
If I have to open two different PRs, update a private npm package, and wait for a Dependabot notification just to change a button style across two products, then our code is unhealthily locked together. That’s where a monorepo actually makes sense. But if every app has its own release cycle, its own secrets, and almost no shared UI, a monorepo will likely just give me merge conflicts and a massive local installation cost.
Before I decide to migrate, I usually check a few things: First, are there actually shared components? I'm talking about a design system, API clients, or linting configs—not just "maybe we'll need this." Second, how often does a single feature change require updates across multiple modules? If it's more than once a month, that's a strong signal. Third, are the languages and tooling the same? Using TypeScript and Node makes managing the workspace much smoother. If you start mixing in Python or other languages, the benefit of uniform tooling disappears quickly. And finally, is there someone responsible for managing the root? Without someone guarding the CI rules or the package manager, a monorepo just becomes a burden for everyone.
If three of those points aren't met, I'd rather stick to separate repositories. I'll only extract a small package if the constant copy-pasting starts to actually bother me.
When I do end up using a monorepo, I try to keep the structure simple. My golden rule: apps can consume packages, but packages should never import apps. It sounds trivial, until someone does a "quick import" from a web app into a package, and suddenly you can't test a single button without spinning up the entire system.
Another thing: don't treat everything as "shared code." Once you have a
packages/ folder, the temptation to dump every utility function in there is
huge. But shared code needs a good reason to exist. If it turns out only one app
uses that function, don't make it a shared package—just put it back where it
belongs.
Not everything needs to be published with complex semver. For internal packages, it's better to use workspace protocols so changes can be atomic. And most importantly: manage your CI from day one. You don't want to reach month six where every tiny PR triggers a test run for the entire system. Use filters to only run tests on the packages that actually changed.
At the end of the day, a monorepo is just a tool, not the end goal. It can be incredibly helpful, but if you make the wrong moves, it just adds complexity that you probably didn't need in the first place.
Back to the editor.