Skip to content
All posts

Microservices Didn’t Scale Your Product. They Scaled Your Regret.

May 31, 2026·Read on Medium·

The correct default for teams under fifty engineers has always been a modular monolith, and the companies that ignored the trend proved it.

Read here for FREE.

The conversation is always the same. Someone in the room draws a diagram. Boxes and arrows. Services communicating over HTTP. Usually it is the most recently hired senior engineer, still warm from a stint at a company that actually runs distributed systems at scale. “We could have the auth service here, the notification service there, and payments in its own domain.”

Everyone nods. It looks right. It feels like serious engineering.

Six months later, nobody can deploy a single feature without touching four repositories. The staging environment is permanently broken because service B expects service C at a version that nobody updated. The on-call rotation now requires a detailed knowledge of Kafka topic offsets. And the two engineers who understood how the services were supposed to talk to each other have both moved on.

I have watched this happen to more teams than I want to count. I have watched it happen to teams I was on. And the pattern is consistent enough that I will say it plainly: if your team has fewer than thirty engineers and your product is under five years old, microservices are probably not the right architectural decision. They might be the most expensive one you ever make.

The Pitch Always Sounds the Same

Microservices get proposed for the same reasons, at the same moments. The codebase has gotten messy. Deployments are getting slower. Two teams stepped on each other’s toes in a PR conflict last sprint. Someone read about Netflix and got inspired.

The pitch has real merit buried inside it. Modularity is genuinely important. Independent deployability is a legitimate goal. Team autonomy scales better than constant coordination overhead. None of those ideas are wrong.

The problem is the leap from “we need better boundaries” to “we need separate services.” That leap assumes the problem is organizational when it might just be that nobody sat down and drew proper module boundaries inside the codebase when it still fit in one head.

Martin Fowler wrote about this pattern and called it MonolithFirst. His observation: almost all the successful microservice stories he had seen started with a monolith that got too big and was broken up deliberately. Almost all the projects that started as microservices from day one ended in serious trouble. The team at Netflix would not thank you for using their architecture as justification for what you are about to build. Netflix had thousands of engineers and a decade of production data before they made those architectural calls.

You have a five-person team and a product that launched eight months ago. That context matters.

What Service Proliferation Actually Costs

There is an accounting that rarely makes it into the whiteboard session.

When you split a feature across two services, you introduce a network boundary where there used to be a function call. That boundary needs authentication, retry logic, circuit breakers and timeout handling. The latency budget for that operation just got a network round-trip more expensive, at minimum. Under normal load that is manageable. Under the traffic spike where you actually need reliability, that is where you find out what your retry logic does when both services are struggling simultaneously.

Distributed transactions are worse. A monolith can wrap a user registration and the associated billing record creation in a single database transaction. With two services you now have a saga pattern to implement, compensating transactions to think through, and a class of bugs that only appear when the payment service comes back up three seconds after the user service believes the user was successfully created. These bugs are not fun to debug in production. They are not fun to write tests for either.

Then there is the operational overhead that compounds slowly until it is overwhelming. Each service is a deployment pipeline, a set of environment variables, a logging configuration, a health check endpoint, a scaling policy and a monitoring dashboard. A team of five engineers running fifteen services is not doing product development anymore. They are doing platform engineering, whether they acknowledged that upfront or not.

Teams are often genuinely surprised when the quarterly cloud bill comes in 40% higher after a microservices migration. They moved the same workload from one process to twenty, added the network overhead and the sidecar containers and the service mesh configuration, and then acted puzzled at the result. The architecture did not reduce complexity. It distributed it across more systems where it became harder to trace.

The Companies That Did Not Touch the Architecture

Shopify runs one of the largest Rails codebases on the planet. Over 2.8 million lines of Ruby, 500,000 commits, under continuous development since 2006. On Black Friday 2024, the platform processed 173 billion requests and peaked at 284 million requests per minute. This is not a microservices architecture. It is a modular monolith, with strict component ownership and a tool called Packwerk that enforces dependency boundaries at the code level before a PR can even merge.

Stack Overflow serves more than 2 billion page views a month from nine on-premise web servers and a single SQL Server. Nine servers. Each one handles roughly 450 requests per second at around 12% CPU utilization. The codebase is a monolith. The infrastructure bill is not modest by absolute terms, but per unit of traffic it is impressively lean.

I am not saying these companies never spin up additional services for specific workloads. They do. What I am saying is that neither company decided to fragment their core architecture into dozens of services because the whiteboard diagram looked architecturally correct. They got the boundaries right inside the codebase first, and they extracted services only when there was a demonstrated operational reason to do so. Not a theoretical one. Not a “we might need this someday” one.

The Modular Monolith Is Not a Consolation Prize

Engineers who have never worked at a company running microservices at production scale tend to treat the modular monolith as a fallback position, a temporary compromise on the way to the real architecture. Engineers who have worked at those companies tend to treat it as an intentional choice that requires discipline and carries genuine advantages.

The modular monolith is a single deployable unit where the code is organized around business domains, not technical layers. Instead of app/controllers, app/models and app/services cutting horizontally across everything you have built, you have modules like Billing, UserManagement and Notifications. Each module owns its own models, controllers, service classes and migration files. Inter-module communication happens through explicitly defined interfaces, not by one module reaching directly into another's internals.

The discipline part is what makes or breaks it. A modular monolith without enforced boundaries is just a monolith with better folder names. The folders do not stop anything. You need conventions, linting rules or tooling that flags when module A is importing from the internal implementation of module B instead of its public interface. Without that enforcement, entropy wins by the second year, and you end up with a big ball of mud that happens to be organized into directories that once made sense.

The payoff for getting it right is significant. Every eventual service extraction becomes surgical rather than archaeological. The interface already exists. The data is already owned by a single module. You are not spending three weeks mapping which tables are touched by which parts of the codebase before you can safely draw a service boundary.

Drawing Boundaries That Hold

The question that keeps teams honest is not “where should we put this class?” It is “who owns this data?”

A good module boundary is almost always a data ownership boundary. The Billing module owns the subscriptions table. The UserManagement module owns the users table. If the Billing module needs to know a user’s email address to send an invoice, it asks the UserManagement module for it through a defined interface. It does not join across tables in a query that reaches into user management’s data as if the boundary did not exist.

This sounds strict because it is. Sloppiness here is what turns a structured codebase into a mess that nobody wants to touch. The enforcement in practice looks like this:

// Correct: Billing requests data through UserManagement's public interface
$email = app(UserManagementService::class)->getEmailForUser($userId);

// Wrong: Billing bypasses the boundary and reaches directly into UserManagement's data layer
$email = User::find($userId)->email; // called from inside a Billing module class

That second example is not wrong in isolation. It is wrong in context because it creates an invisible coupling. Change how users are stored in UserManagement and you get a surprise breakage in Billing. These surprises accumulate. They become the reason people propose microservices: not because the architecture is fundamentally broken, but because nobody enforced the boundaries when they were easy to enforce.

The boundary is not enforced by a framework automatically. It is enforced by the team treating violations as a real code review concern, not a style preference.

When You Should Actually Extract a Service

There is a short list of legitimate reasons to pull something out of a monolith into its own service, and “it would be cleaner architecturally” is not on it.

The first reason is a fundamentally different scaling profile. If your image processing workload needs GPU instances and your API does not, running them in the same process creates resource contention you cannot tune your way out of. Extract the image processing.

The second reason is an independent release cycle driven by a real business or compliance requirement. If your legal team needs the billing component certified and audited on its own schedule, independent from the rest of the application, that is a real constraint. Extract the billing service.

The third reason is a language or runtime mismatch where the capability gap is large enough to justify the integration cost. If the best tooling for a specific ML inference task exists only in Python and your backend runs PHP, a small Python service with a narrow, well-defined API surface is reasonable. Keep it narrow.

“We want to be able to deploy this independently” is not a reason by itself. Your monolith should already be deployable in minutes. If it is not, that is a deployment pipeline problem and you should solve the pipeline problem instead.
“The codebase is getting large” is not a reason by itself either. A large codebase with clear module boundaries and good test coverage is manageable by a competent team. A network of fifteen services with poorly defined responsibilities and cascading failure modes is not manageable by anyone. It is just chaos that is harder to observe.

The monolith versus microservices argument is usually a proxy for a different conversation: whether the team has the discipline to enforce good boundaries without the architecture forcing the issue mechanically. Microservices do impose certain kinds of discipline. They also introduce failure modes, operational burden and cross-service coordination overhead that will consume engineers who should be building product.

Here is the part worth sitting with: if you cannot enforce a module boundary inside a single codebase, you will not magically enforce a service contract across two deployments. The problem follows you across the network boundary. It just becomes significantly harder to fix.

Start with the monolith. Draw the boundaries correctly the first time. Extract services when you can name the specific operational requirement that extraction solves, not the theoretical one, not the aspirational one. Everything else is cargo-culting the architecture of companies that are ten times your size and had to earn their complexity the hard way.

The boring architecture is usually the correct one. The unfashionable choice is often the one that still works three years later. There is no award for the most distributed system at your company’s size. There is, however, a very real cost.

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
Microservices Didn’t Scale Your Product. They Scaled Your Regret. — Hafiq Iqmal — Hafiq Iqmal