The software industry has a dangerous habit of copying solutions without understanding the problems they were built to solve. This blog is about that habit — and what happens when you finally stop.
The Microservices Dream vs. The Microservices Reality
For the past decade, microservices have been sold as the gold standard of modern software architecture. Break your app into small, independent services. Deploy them separately. Scale them individually. Sleep better at night.
Sounds perfect. And for some teams, it genuinely is.
But here is the part nobody talks about at conferences: a massive wave of engineering teams that adopted microservices are quietly rolling them back. Not because microservices are bad. But because they adopted an advanced solution to a problem they did not actually have yet.
Amazon Prime Video moved a distributed microservices system back into a monolith and cut infrastructure costs by over 90%. Segment consolidated 50+ microservices back into a monolith. Stack Overflow runs one of the most visited websites on the internet on a single, well-optimized monolith.
These are not failures. These are engineering teams that ran the experiment, measured the results, and made the rational decision.
This blog breaks down exactly why this is happening, what the real cost of unnecessary microservices looks like, how to identify if your architecture is working against you, and what a better path forward actually looks like.
What Microservices Actually Promise (And Why Teams Buy In)
Before criticizing the approach, it is worth being honest about why microservices are genuinely appealing.
Independent deployability is the biggest draw. When each service is its own deployable unit, one team can ship updates without waiting on another team. No code freeze. No coordination meeting. No "who merged what into main."
Independent scalability is the second promise. Instead of scaling your entire application when only one component is under load, you scale just that component. Your payment service gets traffic spikes on Black Friday? Scale just that. The rest of the system does not need to grow.
Technology flexibility is another pitch. Each service can use the language and stack that best fits its job. Your data processing service can run Python. Your API gateway can run Go. Your legacy integration layer can stay in Java.
Fault isolation rounds it out. If one service crashes, theoretically the rest of the system stays up. A bad deploy in your notification service should not bring down your checkout flow.
These are real, legitimate benefits. The problem is not the benefits — the problem is the cost that comes with them, and whether that cost makes sense for where your team and product actually are.
The Hidden Costs Nobody Puts In The Slide Deck
Every architectural decision is a trade-off. Microservices give you the benefits listed above, but they demand serious payment in return. Here is what that bill actually looks like.
1. Network Overhead and Latency
In a monolith, service A calling service B is a function call. It happens in nanoseconds, in memory, with no failure modes beyond a software bug.
In a microservices architecture, service A calling service B is an HTTP request (or gRPC call, or message queue publish). It crosses a network. It can time out. It can fail. It adds latency. And when you have 10 services all calling each other in a request chain, that latency compounds.
A 5ms response per service call across 8 chained services adds 40ms of pure network overhead to every user request — before your actual business logic even runs.
2. Distributed Systems Complexity
This is the one that truly breaks teams.
Distributed systems introduce failure modes that simply do not exist in a single process. You now have to reason about:
- Partial failures — what happens when Service B is halfway through a transaction and Service C times out?
- Network partitions — services that cannot reach each other, even though both are running fine
- Data consistency — when your User Service updates a record, how long before your Order Service sees that change?
- Idempotency — if a request is retried, will running it twice break things?
- Circuit breakers — you need logic to stop hammering a failing downstream service
- Backpressure — what happens when upstream traffic exceeds what downstream services can handle?
Every one of these problems requires engineering time to solve properly. None of them exist in a well-structured monolith.
3. Observability and Debugging Complexity
In a monolith, a bug has a stack trace. You open one log file, find the error, fix it.
In a microservices system, a single user request might touch 12 services. When something fails, you need:
- Distributed tracing (e.g. Jaeger, Zipkin, Datadog APM) to follow a request across service boundaries
- Centralized log aggregation (e.g. ELK Stack, Loki) to query logs from all services in one place
- Correlation IDs manually threaded through every request so you can link logs together
- Service maps to understand which services talk to which
Without all of this infrastructure in place, debugging a production issue in a 50-service system can take hours. With a monolith, the same issue often takes minutes.
4. Infrastructure and Cloud Cost
Running 50 services means:
- 50 sets of compute resources (even at minimum spec)
- 50 log streams being collected and stored
- 50 health checks running continuously
- 50 containers with their own memory overhead
- Inter-service network traffic costs (especially in cloud environments that charge for data transfer)
- A service mesh or API gateway to route traffic between services
- A secrets manager entry for each service
- CI/CD pipelines multiplied by 50
A team that pays $3,000/month running a well-optimized monolith can easily find themselves paying $30,000–$50,000/month after a full microservices migration — without any increase in actual user traffic.
5. Cognitive and Operational Load
Every engineer on your team now needs to understand not just their own service, but how it fits into the broader system. Onboarding a new developer is dramatically harder. You cannot just clone one repo and run it — you need to spin up dependent services, configure service discovery, set up local networking, and understand a dozen contracts between services.
This cognitive overhead silently slows down development velocity, which is often the exact opposite of what the microservices migration was supposed to achieve.
The Core Mistake: Copying Netflix Without Having Netflix's Problems
Netflix, Amazon, Uber, Google — these companies built microservices at massive scale because they had massive scale problems.
Netflix was running on a single data center and a single monolith when it experienced a major database corruption incident in 2008 that took down the service for three days. They had hundreds of millions of users. They needed geographic redundancy, independent team deployments across a 1,000+ engineer organization, and the ability to scale individual components in ways a single application could not support.
Their problem was genuinely a distributed systems problem.
Most companies adopting microservices do not have that problem. They have a code organization problem, or a team coordination problem, or a deployment pipeline problem — all of which have much cheaper, simpler solutions that do not require introducing distributed systems complexity.
The question is never "are microservices good?" The question is always "do I have the specific problem that microservices solve?"
How To Know If Your Microservices Are Hurting You
There are clear signals that your service decomposition has gone too far or was done prematurely.
Signal 1: Services that are always deployed together
If Service A and Service B are almost always deployed at the same time, they are not actually independent. They have implicit coupling that you are paying the full cost of distributed systems to manage — without getting the deployment independence benefit.
Signal 2: Services that always call each other synchronously
If your request flow is User Request → Service A → Service B → Service C → Service D before returning a response, you have a distributed monolith. You have the complexity of microservices with none of the benefits. Every hop is a latency hit and a failure point.
Signal 3: Cross-service database transactions
If your code uses distributed transactions (two-phase commit, saga patterns, etc.) to maintain consistency across service boundaries, ask yourself: are the business benefits worth this complexity? For most applications at most stages of growth, the answer is no.
Signal 4: A single-digit engineering team managing double-digit services
Microservices exist to let large teams work independently. If you have 8 engineers managing 40 services, each engineer is responsible for 5 services. That is not independence — that is overhead.
Signal 5: Debugging takes disproportionately long
If your mean time to resolve a production incident has gone up since adopting microservices — not because your system is more complex functionally, but because it is harder to trace issues — that is a direct, measurable cost of your architecture.
Signal 6: Your cloud bill scaled faster than your user base
Infrastructure cost should grow roughly proportionally with usage. If your costs tripled but your users grew 30%, the architecture itself is generating unnecessary expense.
The Modular Monolith: The Architecture The Industry Stopped Talking About
The alternative to microservices is not a tangled, spaghetti codebase where everything depends on everything else. That is a bad monolith, and it is absolutely worth avoiding.
The real alternative is a modular monolith — a single deployable application with well-defined internal module boundaries, clear ownership, enforced separation of concerns, and explicit contracts between modules.
Think of it this way: microservices enforce boundaries at the network level. A modular monolith enforces boundaries at the code level. Both can achieve good separation. One of them does it without adding a network between every boundary.
What a Modular Monolith Looks Like in Practice
/src
/modules
/users
users.controller.ts
users.service.ts
users.repository.ts
users.module.ts ← exposes only what other modules need
/orders
orders.controller.ts
orders.service.ts
orders.repository.ts
orders.module.ts
/payments
payments.controller.ts
payments.service.ts
payments.module.ts
/notifications
notifications.service.ts
notifications.module.ts
/shared
/database
/config
/types
Each module owns its own data access layer, its own business logic, and exposes a clean interface to other modules. Cross-module communication goes through defined interfaces — not internal database queries or direct class instantiation from outside the module.
The boundaries are real. The enforcement is through code review, linting rules, and architecture tests (tools like ArchUnit for Java or dependency-cruiser for JavaScript can enforce that Module A does not import directly from Module B's internals).
The difference is that these boundaries live in one deployed application, one process, one set of compute resources — and they communicate through function calls, not HTTP requests.
Which Services Actually Deserve To Be Separate
This is not an argument to never use microservices. Some services genuinely benefit from being extracted.
Keep separate if the service has fundamentally different scaling requirements. A video transcoding pipeline that needs GPU instances and takes minutes per job should not live in the same process as your fast, lightweight API server. The operational profiles are incompatible.
Keep separate if the service has regulatory or compliance isolation requirements. Payment processing under PCI DSS, healthcare data under HIPAA — these often benefit from strict process and network isolation for compliance and audit purposes.
Keep separate if the service is owned by a completely separate team with no shared codebase. If an entirely different engineering team in a different department maintains a service, a clear API boundary with separate deployment is the right call.
Keep separate if the service needs independent release cycles that are genuinely different from the rest of the system. A machine learning inference service that is updated by a data science team on a different cadence from the main product is a reasonable extraction.
The key question to ask every time: Is this service separate because it has a different operational profile, or is it separate because someone drew a box on an architecture diagram?
A Framework For Making The Architecture Decision
Use these questions before committing to any architectural direction.
| Question | Microservices Makes Sense | Monolith Makes Sense |
|---|---|---|
| How many engineers? | 20+ actively shipping | Under 15 |
| How well-understood are your domain boundaries? | Stable, clear, proven over time | Still evolving, new product |
| What is your deployment frequency? | Multiple teams, multiple times/day | One team, a few times/week |
| What is your current scale? | Millions of users, heavy load | Growing, under 500k users |
| Do you have platform engineering capacity? | Dedicated platform/infra team | Developers wear multiple hats |
| What is your operational maturity? | Distributed tracing, observability in place | Basic logging and monitoring |
If the answers lean consistently toward the right column, start with a modular monolith. You can always extract services later — once the boundaries are well-understood and the team genuinely needs the independence.
Extracting a service from a clean modular monolith is a well-understood, manageable engineering task. Collapsing a poorly-conceived microservices architecture back into something coherent is significantly harder.
The Performance Reality
Beyond cost, there is a direct performance story here that often gets overlooked.
Modern hardware is extraordinarily fast. A well-optimized monolith running on a single large instance can handle more throughput than most products will ever need.
Stack Overflow handles over 1.3 billion page views per month with a handful of servers running a monolith. They have published detailed benchmarks showing their primary SQL server idles at around 10% CPU under normal load.
The argument for microservices-based performance usually comes down to horizontal scaling — the ability to spin up more instances of a single service under load. But you can also horizontally scale a monolith. Running 4 instances of your monolith behind a load balancer is a completely valid, simple, and well-understood approach to scaling.
The performance case for microservices becomes real only when different components of your system genuinely have different resource profiles and different traffic patterns that justify running them on different hardware configurations. For most applications, that is not the reality.
What The Real Cost Comparison Looks Like
Here is a realistic infrastructure cost comparison for a mid-sized SaaS product with approximately 50,000 active users.
Microservices architecture (40 services):
| Cost Category | Monthly Estimate |
|---|---|
| Compute (ECS/Kubernetes, 40 services) | $8,000 – $12,000 |
| Load balancers (one per service) | $3,000 – $5,000 |
| Log aggregation and storage | $2,000 – $4,000 |
| Service mesh / API gateway | $1,500 – $3,000 |
| Distributed tracing infrastructure | $1,000 – $2,000 |
| Inter-service networking costs | $500 – $1,500 |
| Total | $16,000 – $27,500 |
Modular monolith (2–3 services: monolith + background worker + media processor):
| Cost Category | Monthly Estimate |
|---|---|
| Compute (2–3 large instances) | $800 – $1,500 |
| One load balancer | $200 |
| Centralized logging | $300 – $600 |
| Basic monitoring | $100 – $300 |
| Total | $1,400 – $2,600 |
The same product. The same users. The same features. A cost difference of 10–15x.
What "Moving Back" Actually Requires
If you are in the situation of managing an overengineered microservices system and considering consolidation, here is a practical approach.
Step 1: Audit coupling.
Map which services call each other. Any pair of services that communicate synchronously on the critical path and are deployed together more than 80% of the time is a merge candidate.
Step 2: Identify data ownership.
The hardest part of merging services is unifying databases. Start by merging services that already share a database, or where one database is a clear superset of another. These are the low-hanging fruit.
Step 3: Merge one pair at a time.
Do not attempt a big-bang consolidation. Pick the two most tightly coupled services and merge them into one module within a shared codebase. Deploy that. Measure it. Then continue.
Step 4: Keep the interfaces.
Even after merging, keep the internal module interfaces clean. The fact that it is now one deployed service does not mean the internal code structure should collapse. Maintain clear module boundaries — you might extract again someday if scale genuinely demands it.
Step 5: Decommission completely.
Once the merged version is running stably in production, decommission the old individual services. Delete the repositories, remove the CI/CD pipelines, close the log streams. The goal is to actually reduce operational surface area, not just merge code while keeping all the old infrastructure running in parallel.
Microservices vs Modular Monolith: Side-by-Side Comparison
| Factor | Microservices | Modular Monolith |
|---|---|---|
| Deployment | Independent per service | Single deployment unit |
| Scaling | Per-service granularity | Horizontal scaling of full app |
| Debugging | Requires distributed tracing | Single log file / stack trace |
| Team independence | High (each team owns a service) | Medium (shared repo, clear modules) |
| Infrastructure cost | High | Low |
| Operational complexity | High | Low |
| Onboarding new devs | Slow (multiple repos, service mesh) | Fast (one repo, one local run) |
| Technology flexibility | High (per-service stack) | Low (shared runtime) |
| Network latency | Present on every service call | Zero (in-process calls) |
| Data consistency | Hard (distributed transactions) | Easy (shared database) |
| Best for | Large teams, stable domains, high scale | Small/mid teams, evolving product |
The Broader Lesson About Architecture Decisions
Architecture decisions should be driven by specific, measurable problems — not by industry trends, conference talks, job posting requirements, or what the large tech companies do.
Every architecture has trade-offs. The job of a good engineer is not to pick the most sophisticated option available. It is to pick the option that best fits the current constraints: team size, product maturity, operational capacity, budget, and expected growth trajectory.
A well-structured modular monolith that ships features quickly, runs reliably, and costs $2,000/month to operate is a better technical decision for most products than a microservices architecture that costs $25,000/month, requires a dedicated platform team to maintain, and makes debugging a multi-hour exercise.
Simplicity is not a compromise. In engineering, simplicity is one of the hardest things to achieve and one of the most valuable things to protect.
The engineers who built the original Unix philosophy had it right: do one thing, do it well. That applies not just to the services themselves but to the decision of how many services you actually need.
Build the simplest architecture that solves your current problems well, with room to evolve as your real problems grow. That is not settling. That is engineering judgment.
Key Takeaways
- Microservices solve specific, large-scale organizational and operational problems. They are not a default best practice for every product at every stage.
- The hidden costs are real and compounding. Distributed systems complexity, debugging overhead, infrastructure cost, and cognitive load add up fast.
- A modular monolith gives you clean boundaries without the operational overhead of a distributed system. Most products are better served by it during their growth phase.
- The decision should be data-driven. Assess team size, domain maturity, scale, and operational capacity. Let those answers drive the architecture.
- Consolidation is a legitimate engineering decision. If you are already in microservices and feeling the pain, audit coupling, merge incrementally, and keep internal module boundaries clean.
- The goal is always maximizing value delivered to users per unit of engineering effort. Sometimes that means microservices. More often than the industry admits, it means a very good monolith.
Are you running microservices that work brilliantly, or ones that are costing more than they give back? Drop your architecture setup and team size in the comments — real data from real engineering teams is more useful than any conference talk.
Top comments (0)