Microservices Are a Solved Problem? Not Quite.
Microservices have been mainstream long enough that the backlash has had its own backlash. First everyone adopted them, then everyone wrote articles about how they had been wrong to adopt them, then a second wave wrote about how the first-wave criticism was too broad.
What’s left after all of this is a clearer understanding of the tradeoffs - one that neither the original hype nor the backlash captured correctly. Microservices are not a universal solution. They are not a failed pattern. They are a set of tradeoffs that are appropriate in some contexts and not in others, and the industry still gets the tradeoffs wrong in predictable ways.
What the Pattern Gets Right
Microservices solve real problems that become acute at sufficient scale and team size.
Independent deployability. A change to the billing service can be deployed without touching the notification service. In a monolith, every deployment deploys everything. For a small team, this is fine. For a team of 20 engineers making changes simultaneously, the coupling in a monolith becomes a deployment bottleneck.
Technology heterogeneity. Different services can be written in different languages and use different databases. If your recommendation engine benefits from Python’s ML ecosystem and your API benefits from Go’s performance characteristics, you can have both.
Failure isolation. A bug in the recommendation service doesn’t take down checkout. Services fail independently, and - when designed correctly - the system degrades gracefully rather than failing catastrophically.
Team alignment. Conway’s Law is real: organizations build systems that mirror their communication structure. Small, independent teams building services they fully own produce cleaner interfaces than large teams building a monolith where ownership is unclear.
These are real advantages. They come with real costs.
What Teams Get Wrong
The distributed monolith
The most common microservices failure mode: a monolith refactored into services that are still tightly coupled. The services call each other synchronously, share a database, or have implicit behavioral dependencies. You’ve taken the complexity of distribution - network latency, partial failures, distributed transactions - without getting the benefits of actual independence.
The tell: a change to service A requires simultaneous deployment of service B, or requires knowledge of service B’s implementation details.
This happens because the decomposition was done by technical layer (user service, product service, order service) rather than by business capability. Technical layer decomposition looks logical on a whiteboard and falls apart when you discover that most operations need data from multiple layers.
Premature decomposition
You are probably not Netflix. Netflix’s microservices architecture was built to solve Netflix’s problems: massive scale, hundreds of engineers, the need to deploy different parts of the system independently hundreds of times per day.
For a startup with five engineers building a product that hasn’t found product-market fit yet, microservices add operational overhead (service discovery, distributed tracing, network reliability, deployment complexity) without the scale that makes that overhead worthwhile. The overhead is real; the benefits are theoretical at small scale.
The principle: start with a modular monolith. If you reach the scale and team size where monolith deployment is a real bottleneck, extract services along genuine business capability boundaries at that point.
Underestimating distributed systems complexity
A function call and a network call are not the same thing. A network call can fail in ways a function call cannot: timeouts, partial failures, connection failures, the called service returning 200 but having failed to commit. Distributed systems require thinking about these failure modes explicitly.
Many microservices implementations treat inter-service calls like function calls. The result: cascading failures, inconsistent state when a downstream service fails partway through an operation, no retry logic, no circuit breaking.
Correct distributed systems patterns - idempotency, circuit breakers, timeouts with fallbacks, saga patterns for distributed transactions - add substantial complexity. Teams that adopt microservices without this background adopt the failure modes of distributed systems without the patterns to handle them.
Shared databases between services
If two services share a database schema, they are not independent. A schema migration for one service potentially breaks the other. The services are coupled at the data layer even if they’re separated at the network layer.
This is common because it’s the path of least resistance. You already have a database. Both services need the same data. Giving them direct access seems simpler than building an API between them.
The correct pattern: each service owns its data. Other services access that data through the service’s API, not directly. If service A needs data from service B’s domain, it calls service B’s API. This enforces the boundary that makes services actually independent.
The cost: this requires data that would have been a join in a monolith to become multiple API calls or event-driven updates. This adds latency and complexity. That cost is real and should be weighed explicitly.
Where the Pattern Is Appropriate
Microservices make sense when:
- The system is large enough that team coordination in a monolith is a real bottleneck
- Different parts of the system have genuinely different operational requirements (different scaling needs, different deployment cadences)
- You can draw clean boundaries along business capabilities with minimal cross-service data dependencies
- Your team has the operational maturity to run a distributed system (monitoring, distributed tracing, deployment automation)
Microservices don’t make sense when:
- You’re still figuring out what the product is (domain boundaries are hard to get right before the domain is understood)
- The team is small enough that monolith deployment isn’t a bottleneck
- You don’t have the operational infrastructure to support multiple independently deployed services
- The main justification is “this is how modern systems are built”
What “Solved” Would Actually Look Like
The pattern has accumulated real knowledge: domain-driven design for identifying service boundaries, the saga pattern for distributed transactions, the strangler fig pattern for migrating from monoliths, service mesh tools for handling cross-cutting concerns like authentication and observability.
These are genuine advances. They make the pattern more tractable than it was in 2015 when most teams were learning by failing.
What hasn’t been solved is the judgment question: when to apply the pattern and how to draw the boundaries. These depend on your specific domain, team, and scale. No tooling or framework replaces that judgment.
The teams that use microservices well understand the tradeoffs and have made a conscious choice. The teams that use them poorly adopted the pattern because it sounded right and are now managing a distributed monolith they don’t fully understand.
The architecture is not the problem. The lack of honest tradeoff analysis before adopting it is.