Microservices sound cheap when you describe them on a whiteboard. Take a large application, identify a few domains, split them into independently deployable services, give each service its own database, and let teams work independently. Beautiful. Then you actually build the thing.
Your five services become twelve. Twelve become thirty. A couple of years later you have eighty repositories, fifty pipelines, dozens of databases, multiple message brokers, several different ways of doing authentication, dashboards nobody looks at, alerts nobody understands, and a production incident where six engineers spend three hours figuring out which service actually broke.
The funny part is that the original application probably wasn’t particularly complicated.
We have spent years talking about how microservices help companies scale, but we don’t talk enough about the opposite side of that equation: every microservice you create permanently increases the cost of building, operating and understanding your system.
And that cost compounds.
Affiliate promo
If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!
A Microservice Is Not Just Another Project
When people estimate the cost of splitting something into a microservice, they usually think about the code.
“We’ll move this logic into a separate service. That’s probably a few days of work.”
No. The code is often the cheapest part.
The moment that code becomes a separate deployable unit, you have created another thing that needs to be built, tested, deployed, monitored, secured, versioned, configured and supported.
You probably need another repository. Another CI pipeline. Another deployment pipeline. Another container image. Another set of environment variables. Another service account. Another secret configuration. Another dashboard. Another health check. Another set of alerts. Another deployment strategy. Another dependency graph entry. Another place where certificates can expire. Another thing your developers need to run locally.
If the service has its own database, add migrations, backups, connection management, monitoring, retention policies and disaster recovery.
If it communicates asynchronously, now you need queues or topics, retry policies, dead-letter queues, idempotency handling, message versioning and tooling that allows you to understand what happened when messages inevitably fail.
If it communicates synchronously, congratulations. You just replaced a method call with a network call.
Something that used to look like this:
var customer = customerService.GetCustomer(customerId);
can now involve DNS resolution, a load balancer, TLS, authentication, serialization, network latency, retries, timeouts, circuit breakers, distributed tracing and a second deployment that may or may not currently be healthy.
The business operation didn’t become more valuable. The architecture just became more expensive.
The Cost Grows Faster Than the Number of Services
Ten services are not twice as complicated as five services, because the difficult part isn’t the services themselves. It’s the relationships between them. Imagine you have a checkout process involving Orders, Payments, Customers, Inventory and Notifications. On a diagram it looks wonderfully clean, until someone asks a very reasonable question: “Why did order 98472 fail yesterday at 14:37?”
Now the fun begins. The Orders service says it successfully created the order. Payments says the payment was authorized. Inventory received a reservation message but timed out communicating with another dependency. A retry occurred thirty seconds later. Meanwhile, the Orders service published another event, which Notifications consumed before the inventory failure had propagated. You now have logs across several systems, distributed traces, correlation IDs, asynchronous events and potentially multiple versions of the same operation happening at slightly different times.
This is the reality of distributed systems. We didn’t make debugging disappear. We distributed the debugging too. In a monolith, I can put a breakpoint somewhere, step through the operation and watch the state change. In a distributed architecture, I sometimes need to reconstruct a crime scene.
You Pay the Infrastructure Tax Forever
One of the biggest mistakes companies make is treating the migration to microservices as a one-time engineering investment. It isn’t. The migration might be temporary, but the operational cost is permanent. Every service needs maintenance. Dependencies need updates. Runtime versions need upgrading. Vulnerabilities need patching. Deployment pipelines break. Infrastructure APIs change. Logging libraries get replaced. Authentication systems evolve. Telemetry standards change.
Suppose upgrading one .NET application to the next version takes a team one day. Now suppose you have eighty independently deployed .NET services. Even if each upgrade is trivial, you still have eighty projects to verify, eighty pipelines to run, eighty deployments to coordinate and eighty opportunities for something weird to happen.
People love saying that microservices allow services to evolve independently, and that is true. They also allow them to become independently outdated. Eventually you find yourself running four versions of the runtime, three generations of your internal libraries and two different messaging abstractions because one team migrated while another didn’t. Congratulations. You achieved independence.
“But AI Makes Development Cheap Now”
This is probably the newest argument for not worrying about complexity. AI can write the boilerplate. AI can create the service, generate the Dockerfile, write the Kubernetes manifest, create integration tests and even help diagnose production issues. All true. It still doesn’t make complexity free.
AI changes the cost curve, but it doesn’t eliminate it. Every additional service creates more code and, more importantly, more context. If I’m asking an AI coding agent to understand a monolithic application, I can point it at the repository and say, “Trace what happens when a customer places an order.” It can search the codebase, follow the calls and build a reasonable picture.
Now try that in a system where the same operation crosses seven repositories, three HTTP APIs, four event types and two infrastructure repositories. The agent needs to understand all of them. Which version of each service is currently deployed? Which event contract does production use? Where is the retry configured? Is the timeout in application code, an API gateway or the service mesh? Which repository owns the shared contract? Which Terraform project creates the queue? Is there another consumer of that event somewhere else?
AI has context windows and token costs. Even when those limits become much larger, feeding a system ten times more architecture still gives the model ten times more architecture to reason about. You can make agents faster, but you cannot make unnecessary complexity stop being complexity.
This becomes especially noticeable with coding agents that operate across large repositories and organizations. The more fragmented your system is, the more time they spend discovering where things live, finding related services, reading contracts and rebuilding context that would have been obvious if the code lived together. Humans suffer from exactly the same problem. We’ve just given the problem to the robots as well.
Microservices Multiply Development Work
Let’s say you want to add a seemingly simple feature: customers can now have a preferred delivery location. In a well-structured monolith, this might involve changing a model, adding a migration, updating a couple of application services, adding an endpoint and writing some tests.
In a microservice architecture, you first need to answer a completely different set of questions. Which service owns that information? Does Orders need a copy? Does Shipping need a copy? Should Shipping call Customers every time it needs it, or should Customers publish an event? What happens if Shipping hasn’t received that event yet? Do we need eventual consistency? What happens when the address changes while an order is being processed? Do we store a snapshot? Does the event contract need a new version? Do existing consumers tolerate the new field?
None of these questions are necessarily bad questions, but notice what happened. We turned “add a preferred delivery location” into a distributed systems design meeting. Architecture is supposed to help us manage complexity. Sometimes architecture is where the complexity came from.
Testing Gets Expensive Too
Microservices are often sold alongside the idea that every service can be tested independently. Again, technically true, but users don’t use your services independently. They use your system. Eventually you have to verify that Orders works with Payments, Payments works with Customers, Customers publishes the event Inventory expects and the version of Inventory currently deployed still understands it.
Unit tests won’t catch that, so you introduce integration tests, then contract tests, then end-to-end tests, then test environments containing twenty services. Someone opens a pull request that needs an environment. Five developers need five environments. QA needs another one. Suddenly your cloud bill starts looking interesting.
And despite all of that testing, production still behaves differently because distributed systems have timing problems, network failures, retries and partial failures that are surprisingly difficult to reproduce. A method call either happened or it didn’t. A message might have happened once, twice or thirty seconds later. Welcome to distributed systems.
Observability Becomes a Product of Its Own
Once you have enough services, logs stop being enough. Now you need centralized logging, metrics, distributed tracing, correlation IDs, dashboards and alerting, and somebody needs to maintain all of those things. None of this is optional. If a request travels through eight services and you don’t have excellent tracing, debugging production becomes guesswork.
So now your company has effectively built another internal product whose only purpose is helping engineers understand the architecture they built. Think about that for a second. We create complexity, then spend millions building tools to observe the complexity, then hire platform engineers to maintain the tools that observe the complexity. And somehow the original system might still be a CRUD application.
Organizational Complexity Is Still Complexity
One of the strongest arguments for microservices is organizational scaling. Amazon famously popularized the idea of small autonomous teams owning independent services, and at sufficient scale this makes sense. If you have thousands of engineers, independent deployment and ownership boundaries can be incredibly valuable.
But most companies aren’t Amazon. A company with fifteen engineers copying an architecture designed to coordinate thousands of engineers is solving a problem it doesn’t have while creating several new ones it definitely does. You don’t need seven services because seven developers are touching the application. You need clear boundaries, and those are not the same thing.
A modular monolith can give you strong domain boundaries, isolated modules, separate schemas, clear ownership and well-defined contracts without making every boundary a network boundary. You can have:
OrdersPaymentsCustomersInventoryShipping
without requiring:
orders.company.internalpayments.company.internalcustomers.company.internalinventory.company.internalshipping.company.internal
A module boundary costs almost nothing. A network boundary costs you forever.
Microservices Also Slow Down Humans
There’s another cost that never appears on the architecture diagram: cognitive load. A new developer joining a monolith may need to understand a large repository. A new developer joining a microservice environment may need to understand fifty small repositories. People sometimes claim the second option is easier because each repository is smaller, but that only works if the developer’s task exists entirely inside one repository.
Real features rarely respect your architecture diagram. The developer fixing checkout needs to know Orders, Payments and Inventory. Then they discover a shared contracts repository, followed by an infrastructure repository. Then they need to figure out the local development setup. Then they learn that Payments can’t run locally because it depends on a cloud resource, so there’s a development environment they need access to. Three days later they still haven’t changed the original line of code.
A 100,000-line application can be intimidating. Twenty 5,000-line services are still 100,000 lines, except now they communicate over the network.
The Worst Microservices Are the Ones With No Reason to Exist
Ask one question about every service in your company: Why is this independently deployable? Not “Why is this logically separate?”, “Why is this a different domain?” or “Why does this have a different folder?” Why does this specific thing need to be deployed independently from the rest of the system?
There should be an actual answer. Maybe it needs to scale independently. Maybe it has radically different availability requirements. Maybe a completely separate team owns it. Maybe it has strict security isolation requirements, genuinely needs a different technology stack or has a release cycle that must be independent. Those are real reasons.
“This is the Customers domain” isn’t one. Domains need boundaries. They don’t automatically need servers.
Start With the Monolith
The default architecture for most new systems should be boring. Build a monolith, structure it properly, create clear modules, keep boundaries explicit and avoid turning the entire application into one giant ball of dependencies. Then watch the system and extract things when you have an actual reason to do so.
If one part needs to scale independently, extract it. If one team becomes blocked by another team’s deployment cycle, consider extracting their domain. If a module needs radically different infrastructure, extract it. If isolation becomes valuable enough to justify the operational cost, extract it. You can always move a module out of a monolith, and doing that is usually much easier than trying to put dozens of services back together later.
This is the part people sometimes misunderstand. Saying “start with a monolith” doesn’t mean “build spaghetti.” A modular monolith can be extremely well designed. The difference is that you haven’t paid the distributed systems tax before you know whether you actually need distributed systems.
Complexity Needs to Earn Its Place
Microservices aren’t bad. Unnecessary microservices are. There are companies where hundreds or thousands of independently deployed services are completely justified because their organizational scale, traffic patterns and team structures make the trade-off worthwhile.
But architecture should respond to problems you actually have, not problems Netflix has, not problems Amazon has and not problems you imagine you’ll have when your startup reaches 100 million users. Every architectural decision introduces a cost. Interfaces cost something. Abstractions cost something. Queues cost something. Services cost a lot.
One of the most expensive mistakes in software engineering is adding complexity because we have convinced ourselves that complexity is what serious engineering looks like. The best architecture isn’t the architecture with the most services. It’s the architecture that lets your company ship software reliably with the least unnecessary friction.
Sometimes that means microservices. Quite often, it doesn’t.
Affiliate promo
If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!

Leave a comment