Somewhere in the life of every growing engineering team there is a meeting where someone says the codebase is getting too big and we should split this into microservices. The room nods, because it sounds like maturity, like the thing real companies do. I have been in that meeting on both sides of the table, and I have learned to slow it down, because the proposal almost always counts the benefits in detail and the costs not at all. A service boundary is not free. Every line you draw between two pieces of code turns a function call into a network call, a single deployment into two, and a clean stack trace into a distributed mystery.
I am not against microservices. We run several at wrxstack, and they earn their place. What I am against is splitting a service because it feels like progress rather than because the split solves a problem you actually have. The cost of a microservice is real, it compounds, and it is paid by your team every day long after the satisfying afternoon when you carved the thing out. Here is the accounting I run before we agree to draw a new boundary, and why most proposed splits do not survive it.
What a service boundary actually costs
Start with the mechanical truth. The moment two pieces of code live in separate services, every interaction between them crosses a network. That network can be slow, it can drop the request, it can return after a timeout that leaves both sides disagreeing about what happened. Work that was previously a deterministic in-process call, impossible to get wrong, is now a distributed systems problem with retries, idempotency, partial failure, and ordering to reason about. You did not remove complexity by splitting. You moved it from inside a process, where the language and compiler help you, to between processes, where nothing helps you and everything is your problem.
Then add the operational tax. Each service is a thing to deploy, monitor, alert on, secure, log, version, and keep on a supported runtime. One service has one of each. Ten services have ten of each, plus the connective tissue to make sense of them together. This is the cost teams forget most reliably, because it does not show up on the day of the split. It shows up six months later as the slow realization that half the team's time goes to keeping the fleet alive rather than building anything.
And add the cognitive tax, which is the worst of the three. In a monolith, a developer can follow a request from entry to database by reading code in one place. Across services, that same request fans out through queues and APIs owned by different teams, and understanding it requires tracing, correlation IDs, and a mental model that lives in nobody's head completely. Debugging goes from reading to archaeology. The first time a production incident spans four services at 2 a.m., you feel the full price of every boundary you drew, all at once.
The reasons that do justify a split
So what makes a boundary worth its cost. In my experience the honest answers are organizational and operational, almost never aesthetic. A split earns its keep when it solves a problem the monolith genuinely cannot, and there are really only a handful of those.
- Independent scaling, when one part of the system needs ten times the resources of the rest and you are paying to scale everything together to feed it.
- Independent deployment under real team friction, when two teams are stepping on each other's releases badly enough that the coordination cost exceeds the boundary cost.
- Hard isolation requirements, when a component must run with different security, compliance, or blast-radius guarantees than the rest.
- A genuinely different technology need, when one workload truly belongs in a different runtime and bridging it inside the monolith is worse than separating it.
Notice that "the codebase is big" is not on that list, and neither is "clean architecture" or "this feels more modular." A large codebase is a real problem, but the cure is usually internal modularity, clear interfaces, and good test boundaries, all of which you can have inside a single deployable. You get most of the design benefit of separation without paying the network, operational, and cognitive taxes. The boundary in the code is cheap. The boundary in the deployment is expensive. Do not confuse the two, because the meeting that conflates them is how teams end up with a distributed monolith, which is the worst of both worlds: all the coordination cost of separate services and all the tight coupling of a single one.
The distributed monolith is the trap
The failure mode I have watched destroy the most velocity is the split that produces services which cannot actually deploy independently. You divide the system into eight services, congratulate yourselves, and then discover that every meaningful feature requires changing four of them in lockstep, releasing them in a specific order, and rolling them all back together when something breaks. You have all the costs of microservices and none of the benefits, because the boundaries you drew did not match the real seams in the problem.
This happens when teams split along technical layers rather than along the lines where the system actually changes. A boundary is only valuable if the two sides evolve independently. If a single business change reliably touches both sides, the boundary is in the wrong place, and the network call you inserted is pure cost with no return. Before any split, I ask the team to look at the last twenty pull requests and trace which proposed services each one would have touched. If most changes stay within one service, the seam is real. If most changes cross several, you are about to build a distributed monolith, and the right answer is to stop.
Example: Early on we considered pulling our notification logic out of the main Atlas backend into its own service, because it felt conceptually separate. Then we traced our recent changes. Almost every notification change came paired with a change to the feature that triggered it, tasks, calendar, inbox. The seam was an illusion. We kept notifications as a well-bounded module inside the monolith with a clean internal interface, and we got every bit of the clarity we wanted with none of the distributed cost. Two years later it is still the right call.
The hidden costs nobody puts in the proposal
Even teams that respect the obvious costs tend to miss a second tier of them, the ones that do not appear in any architecture diagram and never make it into the proposal. The first is data. The moment your data lives behind a service boundary, transactions get hard. In a monolith you can wrap two related writes in a single database transaction and trust they both happen or neither does. Across services you cannot, so you reach for eventual consistency, sagas, compensating actions, and the whole uncomfortable family of patterns that exist only because you cut the data apart. Most teams discover this after the split, when the first cross-service operation needs to be atomic and suddenly is not.
The second hidden cost is local development and testing. A monolith runs on a laptop. A system of a dozen services needs orchestration just to start, mocks or stubs for the services a developer is not working on, and integration environments that are perpetually half-broken. Engineers spend hours fighting their own setup instead of writing code, and the friction quietly slows everyone every day. End-to-end tests that used to be straightforward become flaky exercises in coordinating multiple deployments, so teams write fewer of them, and confidence drops exactly where the system got more complex.
The third is versioning and contracts. Once two services talk over an API, that API is a contract you cannot change freely, because someone on the other side depends on it. You now manage versioning, backward compatibility, and deprecation across team boundaries, with all the negotiation and ceremony that implies. In a monolith you change a function signature and the compiler tells you every caller. Across services you change an endpoint and you hope you found everyone, then you wait through a migration window measured in weeks. These three costs are real, they are large, and they are almost never named in the meeting where the split gets approved.
How we actually decide
When a split proposal reaches me now, I run it through a short, deliberately skeptical checklist. What specific problem does this solve that internal modularity cannot? Which of the four legitimate reasons applies, named explicitly, not gestured at? Will the two sides genuinely deploy and change independently, proven by looking at real history rather than by intuition? Who owns the operational burden of the new service for the next three years, and have they agreed? And what is the rollback story when a change spans the boundary and goes wrong?
I also insist that the team name the alternative they are rejecting. Almost always the honest alternative is a clean internal module with a strict interface, and almost always it would deliver most of the benefit at a fraction of the cost. Forcing that comparison into the open changes the conversation, because a split stops looking like the obvious next step and starts looking like the expensive option it actually is. When the only thing being compared is "split or stay messy," the split wins by default. When the comparison is "split or modularize properly inside the monolith," the split has to genuinely earn it, and most do not.
If the proposal cannot answer those crisply, the answer is not yet. Not no forever, just not yet, because a premature split is far harder to undo than a delayed one. Merging two services back together after you have built tooling, dashboards, and team habits around them is a brutal project that nobody ever has time for. Keeping a module inside the monolith and splitting it later, when the need is undeniable and the seam is obvious, is comparatively easy. The asymmetry should make you conservative. Default to one deployable with clean internal boundaries, and make the system prove it needs to be split rather than assuming it does.
The cost is the point
I want engineers to feel the cost of a service boundary as viscerally as they feel the appeal of one, because the appeal is loud and the cost is quiet until it is not. Every microservice is a standing commitment: another thing to run, another network hop to reason about, another piece of the puzzle in every future incident. Sometimes that commitment is worth it, and when it is, we make it without hesitation. But the question is never "should we use microservices." It is "does this particular boundary solve a real problem that justifies a permanent tax on the team." Ask it that way, count the cost honestly, and most of the splits that felt urgent in the meeting turn out to be ideas you are glad you did not ship.
The teams that scale well are not the ones with the most services. They are the ones who drew the fewest boundaries that mattered most, kept everything else simple, and refused to pay for distribution they did not need. That discipline is unglamorous and it does not make for an exciting architecture diagram, but it is what lets a small team move fast for years. If you want to see the philosophy applied across a large product, it is the same restraint that runs through how we build Atlas: split when the problem demands it, and not one boundary sooner.