Microservices won't fix a bad monolith. They multiply it
Breaking up a messy monolith doesn't fix the design. It spreads the same bad design across the network. Here's why modular monoliths come first.
Developers keep reaching for the same fix. The codebase is a mess, deploys are scary, the team steps on each other. So someone says it: "Let's break the monolith into microservices."
Then the real bill arrives.
Microservices don't fix bad architecture. They spread it. If your monolith is a tangle, splitting it gives you the same tangle, now stretched across a network with latency, retries, and twenty things to deploy instead of one. You took one problem and made twenty copies of it.
Here's the part nobody wants to hear: if you can't build a clean monolith, you won't build clean microservices either. The skill is the same. The network just makes every mistake louder and more expensive.
What developers imagine microservices will do
The pitch sounds great, and on a slide it is:
- Break the monolith and you get instant scalability
- Smaller services mean cleaner architecture
- Independent deploys mean faster shipping
- Teams stop blocking each other
- The old problems disappear
Every line of that is a hope, not a guarantee. Scalability, clean design, fast delivery: those come from how you draw your boundaries, not from how many processes you run. Microservices give you the option to scale a piece independently. They don't hand you good design. You still have to do that part yourself.
What actually happens
I've watched this play out. Here's the list nobody puts on the slide:
- The same bad design, now running over the network
- Latency where you used to have a method call that took nanoseconds
- Data consistency turning into a daily fight, because there's no single transaction anymore
- A failure in one service cascading into three others
- Debugging that means reading logs, traces, and retries across machines instead of one stack trace
- More infra, more config files, more "who owns this service" confusion
- Higher cloud bills and slower local development
- Complexity, multiplied
That latency point is the one people underestimate. A function call inside a single process is basically free. The same call between two services is a network round trip plus serializing the data plus a real chance of timing out. You didn't remove the coupling. You just made it slow and unreliable. This is the kind of cost that's invisible until production, which is exactly the trap I wrote about in What is technical debt: the worst debt is the kind you can't see on the surface.
And data consistency. In a monolith, you wrap a few writes in one database transaction and either all of it lands or none of it does. Split those writes across services and that guarantee is gone. Now you're hand-rolling sagas, compensating actions, and "eventually consistent" states that your support team gets to explain to angry users. That's not a tooling problem you can buy your way out of. It's a design problem you created by drawing the line in the wrong place.
Bad boundaries don't disappear. They get a network address
Picture an order module that reaches straight into the inventory tables and the billing tables. In a monolith that's ugly, but it's fast and it's one deploy. Wrap each of those in its own service without fixing the tangle, and now every one of those reach-ins is an HTTP call that can be slow, can fail, can return half an answer.
The coupling was the actual problem. Splitting the code doesn't remove coupling. It converts a compile-time dependency you could see into a runtime dependency you can't, scattered across a dozen repos.
This is the same trap as picking heavy abstractions before you understand the problem. I made that argument about the front end in runtime UI libraries are tech debt you can't see, and it's the same shape here. Premature structure costs more than no structure.
The move: build a modular monolith first
The good news is the path is simple. Not easy, but simple.
Build one application. Split it into real modules inside that one app: orders, inventory, billing, each with its own clear boundary. Each module owns its data. Other modules talk to it only through a defined interface, never by reaching into its tables. One deployable, clean seams inside.
You get the wins people actually wanted:
- Fast local dev. One thing to run. No fleet of containers just to load a page.
- One transaction still works. Cross-module writes stay consistent because you haven't introduced a network yet.
- One stack trace. A broken request gives you one log to read, not five.
- Real boundaries to test. If your modules are clean, you can poke at the seams and find out where they leak before any of it goes over a wire.
Then you let the services emerge. When one module genuinely needs to scale on its own, or wants its own release schedule, or a dedicated team takes it over, that module gets pulled out into a service. By then you know exactly where the line goes, because the working system told you. You're not guessing on day one.
That's why the best microservice systems aren't designed up front. They're carved out of a modular monolith that already works. Good auth, clean data ownership, sane boundaries: get those right in one process first. The same discipline I pushed in why your auth system is probably wrong applies to the whole architecture. Get the boundary right where it's cheap to fix, then distribute.
When microservices are the right call
To be fair, microservices earn their keep in real cases. Pull a module out when:
- It has to scale independently, like a search or media-processing piece that's a hot path while the rest sits idle
- It needs a different release cadence than the rest of the app
- A separate team owns it and you want their deploys decoupled from yours
- It has a genuinely different runtime profile, like a GPU job or a long-running worker
Notice every one of those is a concrete, observed reason, not a vibe. You'll know them when you hit them. And you'll hit them from inside a monolith that's already doing the work, which is the best possible place to learn them from.
What to do now
If you're starting fresh, start with a modular monolith. Draw hard module boundaries, make each module own its data, and force every cross-module call through an interface. Ship one thing. Watch where it strains under real traffic.
If you're already mid-migration and feeling the pain, stop splitting for a minute. Ask whether the service you're about to extract has a real, named reason to be separate, or whether you're just hoping distribution will clean up a design you haven't fixed yet. If it's the second one, the network won't save you. Fix the boundary first, in the monolith, where it's cheap.
Build the good monolith. The microservices, if you ever need them, will be obvious by the time you do.
Frequently asked questions
Should I start a new project with microservices or a monolith?
Start with a modular monolith. One deployable, clean module boundaries inside it. You get fast local development, easy refactoring, and real data on where the load and the seams actually are. Split out a service only when a specific module has a reason to be separate, like independent scaling or a different release cycle. Designing microservices up front means guessing your boundaries before you know them.
What is a modular monolith?
A modular monolith is a single application split into clear internal modules with strict boundaries. Each module owns its data and exposes a defined interface, and other modules talk to it only through that interface, not by reaching into its tables. It ships as one deployable, so there's no network between modules, but the seams are clean enough that you could later pull a module out into its own service.
Do microservices make an application faster?
Not by default, and often the opposite. A method call inside one process takes nanoseconds. The same call over the network in microservices costs a round trip, serialization, and a chance of failure every time. Microservices can help you scale a specific hot path independently, but the architecture itself adds latency and overhead. Speed comes from good design, not from the number of services.
When do microservices actually make sense?
When you have a concrete reason a piece needs to be separate: it has to scale on its own, it needs a different release cadence, a dedicated team owns it, or it has a fundamentally different runtime profile. Those reasons show up as your system grows. They're hard to predict on day one, which is why the best microservices are usually carved out of a working modular monolith rather than designed up front.
Why is debugging harder with microservices?
In a monolith, a broken request gives you one stack trace in one log. In microservices, that request hops across several services, so you're stitching together logs, distributed traces, and retries from multiple machines to follow one user action. You need tracing, correlation IDs, and centralized logging just to see what a monolith showed you for free.
Building scalable systems and developer-first tools. Lead Software Engineer at DSRPT.
Frequently asked
-
Start with a modular monolith. One deployable, clean module boundaries inside it. You get fast local development, easy refactoring, and real-world data on where the load and the seams actually are. Split out a service only when a specific module has a reason to be separate, like independent scaling or a different release cycle. Designing microservices up front means guessing your boundaries before you know them.
-
A modular monolith is a single application split into clear internal modules with strict boundaries. Each module owns its data and exposes a defined interface, and other modules talk to it only through that interface, not by reaching into its tables. It ships as one deployable, so there's no network between modules, but the seams are clean enough that you could later pull a module out into its own service.
-
Not by default, and often the opposite. A method call inside one process takes nanoseconds. The same call over the network in microservices costs a round trip, serialization, and a chance of failure every time. Microservices can help you scale a specific hot path independently, but the architecture itself adds latency and overhead. Speed comes from good design, not from the number of services.
-
When you have a concrete reason a piece needs to be separate: it has to scale on its own, it needs a different release cadence, a dedicated team owns it, or it has a fundamentally different runtime profile. Those reasons show up as your system grows. They're hard to predict on day one, which is why the best microservices are usually carved out of a working modular monolith rather than designed up front.
-
In a monolith, a broken request gives you one stack trace in one log. In microservices, that request hops across several services, so you're stitching together logs, distributed traces, and retries from multiple machines to follow one user action. You need tracing, correlation IDs, and centralized logging just to see what a monolith showed you for free.