Microservices: The Good, Bad, and Ugly
Microservices promised to solve the monolith problem. A decade later, the industry is learning the hard way that distributed systems have their own costs.
The Promise
Around 2014, a new architectural pattern swept through the software industry like a revelation. Microservices, the idea of breaking a monolithic application into small, independently deployable services, promised to solve many of the problems that plagued large engineering organizations. Teams could work independently. Services could be scaled individually. Different services could use different technologies. Deployments would be smaller and less risky.
The pitch was compelling because the problems were real. Monolithic applications, as they grow, become difficult to understand, difficult to modify, and difficult to deploy. A change in one corner of the codebase can break something in another corner. Deploying the entire application to fix a small bug is slow and risky. Teams step on each other's toes. Build times grow. Test suites become unwieldy.
Microservices seemed like the answer. And for some organizations, they were. But for many others, they introduced a new set of problems that were, in some ways, worse than the ones they solved.
The Good
The genuine advantages of microservices are real and well-documented. For large organizations with multiple teams, microservices provide clear ownership boundaries. Each team owns one or more services, with well-defined APIs. They can develop, test, and deploy independently. This organizational benefit is arguably the strongest argument for microservices, and it aligns with Conway's Law: the structure of a system tends to mirror the structure of the organization that builds it.
Independent deployment is another real benefit. When a service has a bug, you can fix and redeploy just that service without touching anything else. When a service needs to handle more load, you can scale it horizontally without scaling the entire application. When a service's technology stack becomes outdated, you can rewrite it without rewriting everything.
Microservices also enforce modularity. When the boundary between components is a network call, it is much harder to create the tight coupling that plagues monolithic applications. You cannot call a private method across a network. This forced separation can lead to cleaner, more maintainable architectures.
The Bad
The costs of microservices are substantial and often underestimated. The most fundamental issue is that microservices transform every function call between components into a network call. Network calls are orders of magnitude slower than function calls, and they can fail in ways that function calls cannot. The network can be slow, the remote service can be down, requests can time out, responses can be lost. Every service-to-service call needs retry logic, circuit breakers, timeouts, and fallback behavior.
Distributed transactions are another major challenge. In a monolith, if you need to update two database tables atomically, you use a database transaction. In a microservices architecture, if an operation spans two services, each with its own database, you need distributed transaction patterns like sagas or eventual consistency. These patterns are complex, error-prone, and difficult to debug.
Operational complexity increases dramatically. Instead of deploying and monitoring one application, you are deploying and monitoring dozens or hundreds of services. Each service needs its own CI/CD pipeline, its own logging, its own monitoring, its own alerting. You need service discovery so services can find each other. You need a service mesh or API gateway to manage traffic. You need distributed tracing to understand request flows across services.
Debugging becomes harder. In a monolith, you can set a breakpoint, step through the code, and see the entire request path. In a microservices architecture, a single user request might touch ten services, and understanding what went wrong requires correlating logs and traces across all of them.
The Ugly
The ugliest aspect of microservices is what happens when organizations adopt them for the wrong reasons or at the wrong scale. A startup with five engineers does not need microservices. A team that does not have the operational maturity to manage distributed systems should not build them. Yet the hype around microservices led many organizations to adopt the pattern prematurely.
The result is often what developers call a "distributed monolith": a system that has all the operational complexity of microservices but none of the benefits. Services are tightly coupled, requiring coordinated deployments. Shared databases undermine service independence. API changes cascade across the system. You have traded the simplicity of a monolith for the complexity of a distributed system without gaining the organizational benefits that justified the trade.
Another ugly pattern is the "nano-service": services so small they cannot do anything useful on their own. When every function is its own service, you spend more time managing service-to-service communication than building features. The overhead of serialization, deserialization, network latency, and operational tooling dwarfs the actual business logic.
Testing becomes a particular pain point. Unit tests are straightforward, but integration testing across services is hard. Do you run all services locally? Do you use mocks? Do you maintain a staging environment that mirrors production? Each approach has significant drawbacks, and none of them fully replicate the production environment.
The Monolith Strikes Back
In recent years, a correction has been underway. Prominent engineers and companies have begun questioning the default adoption of microservices. Amazon's Prime Video team publicly shared how they improved performance and reduced costs by moving from a microservices architecture to a monolith. Kelsey Hightower, a prominent figure in the Kubernetes community, tweeted: "Monoliths are the future."
The emerging consensus is more nuanced than either "monoliths good" or "microservices good." The right architecture depends on the organization's size, maturity, and specific challenges. A well-structured monolith, sometimes called a "modular monolith," can provide many of the organizational benefits of microservices (clear module boundaries, independent team ownership) without the operational complexity of a distributed system.
The Technology Stack for Microservices
Organizations that commit to microservices must invest in a substantial technology stack:
Service discovery: Services need to find each other. Consul, etcd, and Kubernetes' built-in DNS provide service registries that map service names to network addresses. Without service discovery, you are hardcoding IP addresses, which breaks the moment a service moves or scales.
API gateways: External traffic enters through a gateway (Kong, Ambassador, AWS API Gateway) that handles authentication, rate limiting, request routing, and protocol translation. The gateway provides a single entry point and shields internal services from direct external access.
Distributed tracing: When a request touches ten services, understanding the request flow requires distributed tracing. OpenTelemetry (the merged successor of OpenTracing and OpenCensus), Jaeger, and Zipkin propagate trace IDs across service boundaries, allowing engineers to visualize the complete path of a request and identify bottlenecks.
Circuit breakers: When a downstream service is slow or failing, continuing to send it requests makes things worse. Circuit breakers (popularized by Michael Nygard in "Release It!" and implemented in libraries like Hystrix, resilience4j, and Polly) detect failures and short-circuit requests to failing services, returning a fallback response instead.
Service mesh: Istio, Linkerd, and Consul Connect provide a dedicated infrastructure layer for service-to-service communication. They handle mutual TLS, traffic management, observability, and policy enforcement without requiring changes to application code. The service mesh adds operational complexity but provides capabilities that would otherwise need to be implemented in every service individually.
Container orchestration: Kubernetes has become the de facto platform for running microservices. It provides scheduling, scaling, service discovery, and self-healing. While not strictly required (you can run microservices on VMs or bare metal), Kubernetes' abstractions align well with microservices' operational requirements.
The Data Problem
One of the most challenging aspects of microservices is data management. The ideal is "database per service": each service owns its data and exposes it only through APIs. This provides strong encapsulation and allows each service to choose the best storage technology for its needs (a pattern called polyglot persistence).
In practice, the database-per-service pattern creates significant challenges:
Queries that span services become complex. In a monolith, joining data from users and orders is a SQL query. In microservices, it requires calling two services and joining the results in application code. This is slower, more complex, and harder to optimize.
Transactions that span services require patterns like the Saga pattern (a sequence of local transactions coordinated by choreography or orchestration). Sagas provide eventual consistency but not the ACID guarantees that database transactions provide. Compensating transactions (undoing the effects of a failed step) add complexity and must be carefully designed to handle partial failures.
Data duplication is often necessary for performance. If the order service needs customer information for every order, calling the customer service for every order is expensive. The common solution is to replicate customer data into the order service's database using events. This introduces eventual consistency: the order service's copy of customer data may be slightly stale.
Event sourcing and CQRS (Command Query Responsibility Segregation) are patterns that address some of these challenges by separating write operations (commands) from read operations (queries) and storing all state changes as an immutable sequence of events. These patterns are powerful but add significant architectural complexity.
When Microservices Make Sense
The strongest argument for microservices is organizational, not technical. When a company has multiple teams that need to work independently, microservices provide clear ownership boundaries. Each team owns its services, defines its APIs, chooses its technology stack, and deploys on its own schedule.
The organizational benefit correlates with team size. Organizations with 50+ engineers working on a single product often benefit from microservices because the coordination overhead of a shared codebase exceeds the operational overhead of distributed services. Organizations with fewer than 20 engineers almost never benefit, because the coordination problem is manageable and the operational overhead is not.
A useful heuristic: if your teams can eat lunch together (roughly 8-12 people), you probably do not need microservices. If your engineering organization fills a conference room, you might. If it fills an auditorium, you almost certainly do.
Key Takeaways
- Microservices solve organizational problems (team independence, independent deployment) more than technical ones, and align with Conway's Law.
- The genuine benefits (clear ownership boundaries, independent scaling, technology flexibility) are real but come with substantial costs.
- The costs (network complexity, distributed transactions, operational overhead, debugging difficulty) are often underestimated by organizations adopting microservices.
- The "distributed monolith" anti-pattern (microservices architecture with monolith-like coupling) gives you the worst of both worlds.
- The monolith is making a comeback: well-structured modular monoliths provide many of microservices' organizational benefits without the operational complexity.
- The right architecture depends on team size, operational maturity, and specific scaling challenges, not on industry trends or conference talks.
- The microservices technology stack (service discovery, API gateways, distributed tracing, circuit breakers, service mesh) represents a significant investment that must be justified by concrete organizational needs.
Real-World Migration Stories
Several high-profile case studies illustrate the nuances of microservices adoption:
Netflix is the poster child for microservices success. Their migration from a monolithic Java application to over 700 microservices was driven by their explosive growth and the need for individual team autonomy. Netflix also open-sourced many of the tools they built for managing microservices (Hystrix for circuit breaking, Eureka for service discovery, Zuul for API gateway), establishing patterns that the entire industry adopted.
Amazon famously mandated in 2002 (the "Bezos API mandate") that all teams expose their data and functionality through service interfaces. This early commitment to service-oriented architecture predated the microservices trend by over a decade but established the organizational patterns that later became standard.
Etsy took the opposite approach. While other companies rushed to adopt microservices, Etsy doubled down on their PHP monolith, investing in deployment tools, monitoring, and developer experience. They demonstrated that a well-maintained monolith could deploy 50+ times per day and support hundreds of engineers. Their approach influenced the "monolith-first" counter-movement.
Segment, a customer data platform, publicly documented their journey from microservices back to a monolith. They had adopted microservices early, but the operational burden of maintaining dozens of services with a small team outweighed the benefits. Their blog post "Goodbye Microservices: From 100s of Problem Children to 1 Superstar" became a widely cited cautionary tale.
Shopify adopted a "modular monolith" approach, organizing their Ruby on Rails application into well-defined modules with enforced boundaries but without the operational overhead of separate services. This intermediate approach has gained traction as a pragmatic alternative that captures many of microservices' organizational benefits.
Event-Driven Architecture
Many microservices architectures use events as the primary communication mechanism between services, replacing synchronous HTTP calls with asynchronous message passing through brokers like Apache Kafka, RabbitMQ, or AWS SNS/SQS.
In an event-driven architecture, when a service completes an action, it publishes an event ("OrderPlaced," "PaymentProcessed," "InventoryUpdated") to a message broker. Other services subscribe to events they care about and react accordingly. This decouples services temporally (the publisher does not wait for subscribers) and logically (the publisher does not need to know which subscribers exist).
The benefits are significant: better fault isolation (if a subscriber is down, events queue up rather than causing failures), natural scaling (add more subscribers to handle higher load), and an audit trail (the event stream provides a complete record of what happened). The challenges are equally significant: eventual consistency (subscribers may process events out of order or with delay), debugging complexity (tracing a request across multiple asynchronous event handlers is harder than following a synchronous call chain), and the need for idempotent event handlers (because events may be delivered more than once).
Event sourcing, which stores the full sequence of events rather than just current state, takes this approach further. Instead of storing "the user's address is 123 Main St," you store "the user changed their address to 123 Main St on Jan 15." This provides a complete audit trail and the ability to reconstruct state at any point in time, at the cost of more complex queries and higher storage requirements.
Microservices are not an architecture pattern. They are an organizational strategy with architectural implications. Understanding this distinction is the first step toward making a wise decision about whether to adopt them.
The Organizational Maturity Prerequisite
Microservices require a level of organizational maturity that many teams underestimate. You need robust CI/CD pipelines for every service. You need centralized logging and monitoring. You need on-call rotations that understand distributed systems. You need platform engineering teams that maintain shared infrastructure. You need runbooks for common failure scenarios. Without these capabilities, microservices will cause more outages than they prevent. The rule of thumb is simple: if your team cannot reliably deploy and monitor a monolith, it is not ready for microservices.
Choosing Wisely
The most important lesson from the microservices era is that architectural decisions should be driven by concrete problems, not by hype or industry trends. If you have a small team and a straightforward application, start with a monolith. If you have a large organization with many teams and genuine scaling challenges, microservices might be the right choice, but go in with your eyes open about the costs.
The best architecture is the one that lets your organization deliver value to users effectively. Sometimes that is microservices. Sometimes that is a monolith. Often, it is something in between. The worst architecture is the one you adopted because someone gave a conference talk about it, without understanding your own specific needs and constraints.