The Promise Was Real
The pitch for microservices made sense. You take a large, unwieldy application and decompose it into small, independently deployable services. Each service does one thing. Teams own their services and deploy on their own schedule. If the payment service goes down, the recommendation engine keeps running. You scale the parts that need scaling, not everything.
Netflix, Amazon, and Uber built systems this way and it worked. The architecture became aspirational. By the mid-2010s, microservices were the default answer to “how should we build this,” often without asking whether the question warranted that answer.
The outcome, for a lot of teams, was a distributed monolith with worse tooling, higher operational costs, and failure modes nobody anticipated.
What Decomposition Actually Creates
When you split a monolith into services, you don’t eliminate complexity. You redistribute it. The logic that used to live inside a single process, governed by a single transaction, now lives across a network. And networks fail in ways that in-process function calls do not.
Consider something as routine as a user registration flow. In a monolith, you write a user record, send a confirmation email, and log the event. If the logging step fails, you can roll back the whole transaction. In microservices, you have a user service, an email service, and an events service. They communicate over HTTP or a message queue. The user record might be written successfully while the email service is down. Now you have partial state, a user who exists but hasn’t received their confirmation, and no automatic rollback.
This is the distributed systems problem. It was always there, waiting for you on the other side of decomposition. The microservices pitch often glossed over it.
The failure modes compound. Services call other services, which call others, creating dependency chains that are often undocumented and poorly understood. A latency spike in one service propagates upstream. Without explicit timeout and circuit-breaker patterns, a slow dependency can exhaust the thread pool of every service that depends on it, a failure mode called cascading failure. Teams that migrated to microservices without implementing these patterns discovered them the hard way, usually at 2 a.m.
The Operational Weight Nobody Budgeted For
Running one service requires a deployment pipeline, monitoring, logging, alerting, and on-call rotation. Running thirty services requires thirty of each. The overhead isn’t linear, but it isn’t free either.
Small and medium-sized engineering teams often don’t have the infrastructure sophistication that Netflix or Amazon built before they adopted microservices at scale. They adopt the architectural pattern without the supporting platform. The result is engineers spending a disproportionate amount of their time on infrastructure concerns: container orchestration, service discovery, distributed tracing, secret management. Work that never ships a feature.
Kubernetes solved some of this. It also introduced its own substantial complexity. Teams routinely underestimate the expertise required to operate a Kubernetes cluster reliably in production. The operational costs are real and ongoing.
There’s also a cognitive load cost. Reasoning about a system with thirty services requires understanding how they interact, which is significantly harder than reading a monolithic codebase. Onboarding a new engineer to a microservices architecture takes longer. Debugging a production issue across service boundaries requires distributed tracing tools that need to be set up and maintained. Why fixing a production bug is harder than writing the code is already a difficult problem. Microservices make it materially harder.
When the Service Boundaries Were Wrong
The hardest problem in microservices isn’t operations. It’s getting the boundaries right, and most teams get them wrong the first time.
Service boundaries should align with business domains, a concept from Domain-Driven Design. In practice, teams often split services along technical lines (“the database layer” and “the API layer”) or split prematurely before they understand the domain well enough to draw clean boundaries. When a feature requires coordinating changes across three services simultaneously, that’s usually a sign the boundaries were wrong. You haven’t achieved independence; you’ve just added coordination overhead.
The symptom is what’s often called the distributed monolith: microservices that are tightly coupled, that must be deployed together, and that share databases. You get the operational costs of microservices without the autonomy benefits. It’s the worst of both worlds.
Changing service boundaries after the fact is expensive. It requires migrating data, updating contracts between services, coordinating deployments, and often negotiating across teams who have ownership of their respective services. The engineers who built it are bad at estimating a rewrite applies here with full force.
The Team Structure Problem Is Underneath Everything
Conway’s Law holds that organizations design systems that mirror their communication structure. Microservices work well when your team structure actually reflects the domain decomposition. Amazon famously organized around two-pizza teams, each owning a service end-to-end. The organizational design came first; the architecture followed from it.
Many teams do the reverse. They adopt a microservices architecture without changing how teams are structured, which means services end up with unclear ownership, shared databases, and deployment coordination requirements that defeat the entire purpose. You can’t achieve technical autonomy without organizational autonomy.
When a service has multiple owners, nobody is accountable for its reliability. When a service has no clear owner (which happens when teams reorganize and the architecture doesn’t), you have a liability sitting in production with nobody responsible for it.
How to Know If You’re Already Stuck
There are specific signals that your microservices architecture has become a liability rather than an asset.
The first is synchronous call chains longer than two or three hops. If Service A calls Service B, which calls Service C, which calls Service D, you have tightly coupled distributed logic. Latency accumulates, and failures at D can cascade to A. This suggests the services should probably be consolidated or the communication pattern should be event-driven.
The second is shared databases. If two or more services read and write the same database tables, they’re not actually independent. Schema changes require coordinating multiple services. This is a monolith wearing a microservices costume.
The third is deployment coupling. If deploying one service routinely requires deploying two others in a specific order, your boundaries are wrong. True service independence means deploying on your own schedule.
The fourth is a monitoring and observability gap. If your team can’t answer “which service caused this request to fail” in under five minutes using existing tooling, your operational visibility is insufficient for the architecture you’ve built. This is particularly dangerous because the failure modes that matter most are the subtle ones. Silent bugs are the ones that actually ruin you is a problem that distributed systems amplify.
The Honest Case for Going Back
Amazon itself has been moving some workloads back toward consolidated services. Segment, the customer data platform, famously published a post-mortem of their microservices migration, describing how 140 microservices became unmanageable for their team size. They consolidated back into a monolith and saw significant improvements in reliability and developer velocity. The architecture was the right one for Netflix’s team size. It was the wrong one for Segment’s.
This isn’t a failure of the microservices idea. It’s a failure of cargo-culting an architectural pattern without understanding the conditions that make it appropriate. The conditions are: large teams that need to work in parallel without coordination, well-understood domain boundaries, and existing platform infrastructure to handle the operational complexity. If you don’t have all three, you’re taking on real costs for speculative benefits.
A well-structured monolith with good internal module boundaries can be decomposed later, when the team is large enough and the domain is well-enough understood to draw clean service boundaries. Starting there isn’t a step backward. For most teams at most stages, it’s the correct choice.
What This Means
Microservices are a solution to a specific class of problems: scaling large teams, achieving deployment independence across many parallel workstreams, and isolating failure in high-traffic systems. They are not a general improvement over monolithic architecture. The pattern became fashionable partly because the companies that made it famous were operating at scales that justified the costs, and their success was more visible than the operational complexity behind it.
If your architecture exhibits synchronous call chains, shared databases, deployment coupling, or unclear service ownership, those aren’t configuration problems. They’re design problems, and they get more expensive over time. The honest question is whether the independence you hoped to achieve actually materialized, or whether you distributed a monolith and inherited the worst properties of both.
Audit your dependency graph. Map your service ownership. Time how long it takes your team to isolate a production failure. If the answers are uncomfortable, the architecture earned those answers.