Why I Can’t Stand Monorepos

Monorepos promise simplicity, visibility, and easier refactoring, but in practice they often create more problems than they solve. This post explains why monorepos add unnecessary complexity, blur boundaries, and make Git history painful, while showing when multi-repos actually make more sense. Learn where monorepos work, where they fail, and how to choose the repo structure that truly fits your team and architecture.

RP

Robert Ponsford

Aug 6, 2025

Monorepos are all the rage these days. You hear how they improve visibility, make refactoring easier, and reduce duplication. But here’s the thing: I hate them.

I’ve worked on systems with monorepos and multi-repos, and no matter how many people praise monorepos, my experience has been the same, they make things harder, not easier.

Let me break down why.

Complexity for the Sake of “Convenience”

People love to say that monorepos reduce overhead because you don’t need to create separate pull requests across multiple repos. But in reality, that “convenience” just shifts the complexity elsewhere.

Now you’re writing custom tooling and CI logic to prevent the entire codebase from rebuilding on every commit. You’re managing conditional deploys, tracking cross-service changes, and arguing over what “changed” means when someone edits a shared utility. This isn’t reducing complexity, it’s just centralizing it and making it harder to isolate.

In a multi-repo setup, each repo builds itself. You don’t need to think about what a commit in service-a does to service-b, because it doesn’t.

Poor Separation of Concerns

In a monorepo, boundaries blur. It becomes easy to pull in code from other services that you shouldn’t be depending on, not because you meant to, but because it’s right there.

You start building features with implicit dependencies on other parts of the codebase. You assume something will be there in production just because it’s in your local dev environment. This kind of coupling leads to assumptions, brittle deployments, and ultimately, bad architecture.

In multi-repo land, the division is clear. You have to explicitly define your API contracts, your deployment flows, and your responsibilities. That’s a good thing.

Git History Is a Mess

Ever try to track a bug or understand the history of a single service in a monorepo? Good luck sifting through 50 other unrelated commits from 12 other projects.

Git blame becomes painful. Logs become bloated. Merge conflicts hit unrelated areas of the codebase. Suddenly, your tiny patch to a CLI tool breaks CI for a web service you’ve never touched.

Boilerplate Tradeoffs Are Overstated

People argue that multi-repos introduce more boilerplate, and they’re right, initially. But that boilerplate is usually simple: a Dockerfile, a main.go, maybe a CI YAML file. You can copy-paste that or template it out in 30 seconds.

Monorepos, by contrast, require complex bootstrapping logic to simulate boundaries that multi-repo setups get by default.

Worse, the boilerplate in monorepos isn’t just more dense, it’s also harder to reason about. You end up writing scripts to figure out which packages changed, what needs rebuilding, or how to scope pnpm workspaces or build matrices. It’s like solving problems that wouldn’t exist in the first place if you just split the codebase up properly.

If You Can’t Handle Service Downtime, That’s a Design Problem

One argument I’ve seen for monorepos is that they reduce deployment race condition, e.g., one service changing a contract before another one is updated. But let’s be real: if your system can’t handle partial deploys or temporary mismatches between services, your system is fragile.

Services should degrade gracefully and recover automatically. That’s not a reason to tightly couple your deployment pipeline or your source tree. That’s a reason to improve your architecture.

Monorepos at Scale Make Sense… Until They Don’t

Let me be fair: monorepos can work well, but usually only when you have an organization large enough to dedicate entire teams to managing the tooling, CI, dependency graph analysis, change detection, deployment gating, and all the other internal complexity that comes with it.

At that scale, the cost of coordination across repos might outweigh the simplicity of boundaries. But once you’re spending engineering effort just extending Git, writing meta-build logic, or crafting commit-aware CI pipelines, you’re no longer just building software, you’re building a platform to build software.

That’s fine if you have the scale for it. But most devs, especially solo or small teams, just don’t. And trying to act like Google when you’re three people is a good way to burn time for very little gain.

JavaScript Monorepos: A Justified Exception

Now, here’s the one case where I’ll give monorepos a bit of credit: JavaScript.

The Node.js ecosystem is uniquely painful when it comes to managing internal packages. Publishing packages to a registry, setting up local linking, or even just referencing sibling libraries cleanly across repos can be a mess.

In these cases, monorepos can actually simplify things. Tools like pnpm, turborepo, or nx make it easier to share utilities, components, and types across projects without the need for publishing or weird path hacks. You get workspace-level hoisting, consistent tooling, and local development speed.

So if you’re knee-deep in JavaScript, especially fullstack setups, and you’ve got lots of shared logic, a monorepo might genuinely save time.

That said, the reason it’s justified is because the ecosystem is broken. You’re compensating for the deficiencies of Node’s package and module system. It’s a workaround, not a universal solution.

Debunking the Myth: “Multi-Repos Force You to Avoid a Monolith”

A common claim in favor of multi-repo setups is that they “force” a service-oriented architecture or prevent you from building a monolith. The reasoning goes: if your services live in separate repositories, then they must be cleanly separated and modular. But that’s a myth, you can absolutely build a monolith using multiple repos.

How? Easy:

  • You tightly couple services across repos with hard dependencies.
  • You require all services to be deployed together.
  • You share logic between repos in inconsistent ways (e.g. copy-pasting or git submodules).
  • You rely on synchronous inter-service communication without fallbacks.
  • You version-lock everything so one change requires changes in every repo.

When that happens, you’ve just created a distributed monolith, with all the fragility and tight coupling of a traditional monolith, plus the overhead of managing multiple repositories.

So let’s be clear:

  • Multi-repos do not enforce decoupling.
  • They do not enforce boundaries.
  • And they definitely don’t prevent monoliths.

If anything, they can obscure monolithic behavior behind a false sense of separation. Unless your system is designed to be modular, with clear ownership, interfaces, and autonomy, your repo layout won’t save you.

In short: monorepo ≠ monolith, and multi-repo ≠ microservices. You can (and people often do) build monoliths in both.

Don’t Force Multiple Repos, Just Use What Makes Sense

Let’s be clear: you don’t need multiple repos if you only have one service. There’s no benefit in splitting things up just for the sake of adhering to some “best practice” you saw online. If your entire system is one backend, one frontend, one purpose: keep it together. One repo is fine. Clean, simple, and efficient.

Repo structure should follow architecture, not the other way around.

The goal of multiple repositories is to reflect real separation in responsibility and deployment. If your project doesn’t have that yet, don’t invent artificial boundaries just to be “modular.” You’ll end up with fragmentation, extra boilerplate, and unnecessary friction for no reason.

But when you do have a reason, such as separate services, different release schedules, different teams, different lifecycles, then reach for multiple repos. That’s when the payoff makes sense. You gain autonomy, isolation, and cleaner ownership.

You can also have a frontend and backend either in the same repo or split, and both choices are valid. What you choose should depend on factors like:

  • Are they written in the same language?
  • Do they function as a single tightly-coupled unit or are they independently deployed?
  • Is your frontend just a UI layer for the backend or does it have its own product scope?
  • Are you using a fullstack framework that bundles them together by default (e.g., SolidStart, Next.js)?
  • Does your tooling make local development easier when they’re together or separate?

There’s no “one-size-fits-all” here, the structure should reflect the nature of the code, the team, and the goals.

So no, it’s not “evil” to have one repo. It’s only a problem when your architecture needs separation and your tooling refuses to acknowledge it.

In other words:

👉 Don’t split what doesn’t need to be split.

👉 But don’t lump together what should be separated either.

Final Thoughts

At the end of the day, everything here is just my experience, not gospel. I’m not saying monorepos are bad for everyone, or that multi-repo setups are the objectively superior choice. I’m saying that for me, as someone who’s worked solo and at smaller scale, multi-repos have led to cleaner, more maintainable, more understandable systems.

But you should experiment for yourself.

Try both approaches. See what breaks. See what works. The only real way to learn is to fail a little, and learn from it. Don’t let Twitter threads or Reddit posts (or even this article) dictate how you build your systems.

Your context matters. Your scale matters. Your preferences matter.

So take what’s useful here, discard what isn’t, and build something great. Whether it lives in one repo or twenty doesn’t matter nearly as much as whether it works for you.