You Probably Don't Need Microservices - Modular Monorepo


Microservices have a reputation. They’re what serious companies do. They’re how you “scale.” So when a team starts a new project, there’s a temptation to reach for them from day one — split the app into services, give each one its own repo, wire them together over HTTP or a message broker, and feel like you’ve built something real.

What you’ve actually built is a distributed system. And distributed systems are hard.

The Cost Nobody Talks About

The benefits of microservices are well documented. The costs less so.

Every service call that used to be a function call is now a network request. That means latency, retries, timeouts, and partial failures. Your local development environment now requires running five services simultaneously, probably with Docker Compose, and a change that touches two services means two repos, two PRs, and hoping nothing breaks at the seam. Debugging a bug that lives in the interaction between services is significantly harder than setting a breakpoint in a monolith.

There’s also the operational overhead. Each service needs its own deployment pipeline, its own logging, its own health checks, its own config management. On a small team, this overhead doesn’t get spread across teams — it all lands on the same people.

You end up spending a large portion of your time on infrastructure plumbing that has nothing to do with the product you’re trying to ship.

The Problems Microservices Solve (And What Actually Causes Them)

The two things microservices are genuinely good at are independent deployment and independent scaling.

Independent deployment matters when you have multiple teams that can’t coordinate releases. If the payments team and the recommendations team are blocked waiting on each other to deploy, that’s a real problem. But if you have one team, or a few teams that can communicate easily, this isn’t a bottleneck you have.

Independent scaling matters when different parts of your system have very different load profiles — your search service gets hammered while your user settings service barely moves. But this only matters at a scale most applications never reach.

If neither of those things is a real, felt problem for your team right now, microservices are solving problems you don’t have while introducing problems you do.

What a Monorepo Actually Gives You

One of the most common reasons teams reach for microservices is wanting to split the codebase into manageable pieces. Multiple repos feels like separation of concerns. But you don’t need separate deployable services to get a well-structured codebase.

A monorepo gives you most of what people think they need microservices for:

Clear code boundaries. You can organize your code into packages or modules — packages/auth, packages/billing, packages/notifications — with explicit public APIs between them. Code inside a module is an implementation detail. Code that crosses boundaries goes through the module’s interface.

Faster builds. Tools like Turborepo and Nx understand your dependency graph and only rebuild what changed. Touch the auth package and only the auth package and its dependents rebuild. You get build caching, parallelism, and affected-only test runs without splitting into separate services.

Atomic changes. A refactor that touches three modules lands in one PR. You see the full change, review it in one place, and merge it once. No coordinating across repos.

Shared tooling. ESLint config, TypeScript config, test setup — all defined once and inherited. No drift between services that were set up at different times by different people.

The Modular Monolith

The key idea is to design your application as if it were a set of microservices — clear bounded contexts, explicit interfaces between modules, no direct database access across module boundaries — but deploy it as a single process.

Each module owns its own data. If the billing module needs user information, it calls the user module’s API, not the users table directly. This is the same constraint you’d have with microservices, enforced at the code level instead of the network level.

The result is an application that is well-structured enough to extract a service from when you actually need to, without paying the distributed systems tax until that day comes.

When You Actually Need Microservices

There are cases where microservices are the right call:

  • You have genuinely independent teams that need to deploy on their own schedule, and coordination is actually slowing you down
  • A specific part of your system has load characteristics so different from the rest that you need to scale it independently
  • You’re migrating a legacy monolith incrementally and can’t rewrite it all at once

The important thing is that these are real, observable constraints, not theoretical future ones. If you’re not hitting them today, don’t architect for them today.

The Migration Isn’t as Hard as You Think

One concern people have is that starting with a monolith means you’ll be stuck with one. You won’t.

A well-structured modular monolith is almost trivially easy to extract services from. Because each module already has clear boundaries and explicit interfaces, pulling one out means: deploying it separately, changing the in-process function call to an HTTP call or a queue message, and updating the infrastructure. You’re not untangling spaghetti — you’re cutting along a seam that was already there.

The teams that struggle most with this migration are the ones who built a big ball of mud monolith and then decided they needed microservices. That’s not a monolith problem, that’s a structure problem. A modular monolith doesn’t have that problem.

Start Simple, Extract Deliberately

The default should be: start with a monorepo and a modular monolith. Keep your modules well-bounded. When a specific, real scaling or deployment constraint appears, extract that module into a service.

You’ll ship faster, debug easier, and spend your time building features instead of managing infrastructure. And when the day comes that you genuinely need microservices, you’ll have a codebase that’s already halfway there.