Somewhere around 2014, the software industry decided that monoliths were legacy and microservices were the future. The pitch was irresistible: independent deployability, team autonomy, technology diversity, granular scaling. What the pitch left out was the price tag. Microservices do not remove complexity from a system. They redistribute it across networks, across teams, across deployment pipelines. Distributed complexity is categorically harder to see, harder to debug, and harder to fix than the monolithic kind.
The thesis here is not that microservices are bad. The thesis is that microservices are a tradeoff, not an upgrade, and that most teams adopt them before they need them. Martin Fowler has been making this case since at least 2015 with his "MonolithFirst" principle: start with a monolith, prove the monolith is the bottleneck, and only then extract services. The advice is widely cited and almost universally ignored. Starting with a monolith does not feel ambitious. Nobody has ever gotten promoted for proposing the simple option. But Fowler's point is not that monoliths are inherently superior. His point is that microservices are expensive, and you should not volunteer for costs you have not proven you need to pay. That distinction, between what an architecture can do and what your organization needs it to do, is the one this entire conversation turns on.
The Pitch and the Price
Microservices solve organizational problems, not technical ones. The primary benefit is solving deploy contention: the coordination cost that emerges when multiple teams need to ship changes to the same artifact at the same time. In a monolith, every deploy is a scheduling event. Teams queue behind each other. A broken test in the payments module blocks the team working on search. A merge conflict in shared utility code delays three squads simultaneously. At a certain organizational scale, this coordination cost dominates everything else.
That is the genuine benefit. Here is the price: every function call that was in-process inside a monolith becomes a network call. This is not a minor semantic difference. Function calls are fast, typed, and fail in predictable ways. Network calls are slow, loosely coupled, and fail in ways that are genuinely difficult to reason about. Networks partition. Requests time out. A downstream dependency starts responding 200 milliseconds slower than usual (not failing, just slow) and suddenly the entire request pipeline is backed up, threads are exhausted, and the only symptom end users see is that the application has stopped working for reasons nobody can immediately explain.
Leslie Lamport defined a distributed system as one where the failure of a computer you did not even know existed can render your own computer unusable. That definition is funny because it is precise. In a monolith, the blast radius of a failure is the process. In microservices, the blast radius is every service in the call graph. A partial network partition can cause two services to each believe the other is down while both are running fine. The CAP theorem is not an academic exercise. It is a description of the failure modes your on-call engineer will debug at 2 AM on a Saturday.
The second genuine benefit is team autonomy. Each team owns a service end-to-end, reducing cross-team coordination overhead. Conway's Law in its purest form: organizations design systems that mirror their own communication structures. Not a suggestion. A force of nature, as reliable as gravity and about as negotiable. When Amazon drew service boundaries that matched team boundaries, they were not applying Conway's Law. They were acknowledging that resisting it was more expensive than aligning with it.
The price of that autonomy is data consistency. A monolith with a single database gives you transactions: ACID guarantees, the ability to update two tables atomically and know that either both writes succeed or neither does. Split that database across services and those guarantees evaporate. Now you need sagas, multi-step choreographies where each service performs its local transaction and publishes an event, and if any step fails, you execute compensating transactions to undo work that already succeeded. Eventual consistency is a legitimate architectural choice. But teams that adopt it because "that's what microservices do" without understanding the failure modes are building systems that will lose data in ways they will not notice for weeks.
Technology diversity and granular scaling are the other benefits people cite. They are secondary and frequently overvalued. Technology diversity sounds compelling in an architecture review. In practice, most teams do not need three languages and four databases. They need one stack that everyone understands and can debug at three in the morning when the pager goes off. Granular scaling matters if you have a genuine hotspot, a search service that needs 50x the compute of the user profile service. Most applications do not have load profiles that diverge enough to justify separate infrastructure for each component.
Meanwhile, these secondary benefits carry their own operational costs that compound fast. Debugging a slow request that touches eight services means distributed tracing with tools like Jaeger or Zipkin, correlated logging with request IDs propagated across every service boundary, and centralized metrics dashboards that someone has to build, maintain, and actually look at. Datadog's 2023 survey of microservices adopters found that the median organization running more than 50 services spends over 30% of engineering time on observability and incident response infrastructure. Not a bug. The cost of doing business in a distributed system. Eight services also means eight CI/CD pipelines, eight sets of infrastructure configuration, eight health check endpoints, eight things that can be at incompatible versions. Contract management between services is an ongoing tax. Every service-to-service API is a contract, and breaking changes cascade in ways that are hard to predict and harder to test.
Military logistics discovered the same principle centuries ago: a single supply line serving an entire army creates a chokepoint that limits the speed of every division to the speed of the slowest. Independent supply chains for independent units cost more to operate, but they remove the single point of contention. The tradeoff is worth it only when the army is large enough that the chokepoint, not the enemy, becomes the binding constraint. The parallel to engineering organizations is almost exact. Deploy contention is the supply-chain bottleneck of software delivery.
Here is the honest math. A team of five engineers running 15 microservices will spend more time operating the infrastructure than building features. That is not a failure of the team. It is the natural consequence of operational surface area scaling with service count, not user count. Adding a hundred thousand users to a monolith does not make the deployment more complex. Adding one microservice does. Microservices do not remove complexity. They redistribute it. And distributed complexity is harder to see, harder to debug, and harder to fix.
When the Tax Is Worth It
The case for microservices is strongest when deploy contention is your primary bottleneck. Not your only problem (every engineering organization has a dozen) but the one currently limiting throughput more than anything else. Multiple teams genuinely blocked by shared release cycles. The coordination cost of shipping to a single artifact exceeding the operational cost of running separate ones. This usually starts mattering somewhere around 50 engineers, depending on team structure and deployment frequency. Below that threshold, the monolith's simplicity almost always wins.
But "when" is inseparable from "how." You cannot meaningfully answer whether you are ready for microservices without answering whether you can draw the boundaries. The case requires that your bounded contexts are clearly identifiable. Domain-driven design is not optional here; it is prerequisite. If you can look at your domain and draw clean lines (this is the order domain, that is the inventory domain, notifications are separate, and they interact through well-defined interfaces) then you have the raw material for reasonable service boundaries. If those lines are fuzzy, if the same data needs to be owned by three different services, if your domain model is still shifting because the product has not found market fit, you are not ready. Service boundaries drawn in the wrong place are worse than no boundaries at all. They add all the operational cost of microservices while creating tight coupling across the network. That is the worst possible outcome: you pay for independence and get none of it.
The canonical example is Amazon, though the lesson people draw from it is usually wrong. Jeff Bezos's Two-Pizza Team mandate (no team should be larger than two pizzas can feed) was not an architecture decision. It was an organizational one. Bezos wanted teams small enough to move without waiting on shared release trains or cross-team coordination committees. Service boundaries followed team boundaries, not the other way around. Amazon did not adopt service-oriented architecture because it was technically superior. Amazon adopted it because the company had grown to a point where hundreds of engineers deploying to the same monolith was destroying their ability to ship. The boundaries worked because they matched the org chart. Teams that pretend the boundary question is a technical exercise end up with service boundaries that cross team boundaries, which means every deploy still requires cross-team coordination, and the microservices architecture has bought exactly nothing.
Sam Newman's Building Microservices is the definitive guide on the boundary question, and his central insight is deceptively simple: align service boundaries with bounded contexts from domain-driven design, not with technical layers. A "database service" and an "API service" are not microservices. They are a monolith cut horizontally, and every request still touches every layer. Proper decomposition is vertical. The order service owns the order database, the order API, and the order business logic. It can be deployed, tested, and reasoned about without any knowledge of the inventory service's internals. Data that changes together belongs together. When an order is placed, the order record, the line items, and the order status all change in the same transaction. They belong in the same service. When a customer updates their shipping address, that change should not require coordinating with the order service, the notification service, and the billing service. If it does, the data model is split wrong and no amount of messaging infrastructure will fix it.
Shopify runs one of the largest Ruby on Rails monoliths in the world. It works because they invested in modularization, not decomposition. The discipline of drawing clean module boundaries inside a single codebase gave them every benefit of clear domain separation without any of the distributed overhead. The irony is that this discipline is also the prerequisite for successful microservices. A team that cannot draw clean boundaries in a monolith will not suddenly discover the skill when the boundaries carry the additional penalty of network latency and distributed failure modes.
The case against microservices is simpler. Fewer than three teams deploying to the same codebase? The coordination cost of a monolith is almost certainly lower than the operational cost of distributed services. If your primary bottleneck is feature complexity (the domain is hard, the requirements are ambiguous, the team is still figuring out what to build) microservices do not help and actively hurt, because now you are solving a hard domain problem and a hard infrastructure problem simultaneously. If the motivation is "because Netflix does it," remember that Netflix has thousands of engineers, an in-house platform team that builds custom infrastructure tooling, and operational challenges unique to streaming video at global scale. Twelve engineers and a product that has not achieved product-market fit? That is not Netflix.
There is another motivation that deserves to be named honestly. "We should use microservices" is, like "we should use Kubernetes," almost never a statement about architecture. It is a statement about what the engineer read last week, which conference talk excited them, which technology they want on their resume. That is not a criticism. The desire to work with modern tools is legitimate, and ignoring it will cost you good engineers. But it should be named for what it is: a career development preference, not an architectural argument.
Everything Fails
In a monolith, a function call either returns or throws an exception. The failure modes are well-understood. In a distributed system, a service can be up but slow. A network partition can cause two services to disagree about the state of the world. A downstream dependency can accept your request, acknowledge it, and then fail to process it. A partial success is harder to handle correctly than a clean failure. If you are not designing for these failure modes from day one, you are building a distributed monolith that has not failed yet.
Michael Nygard's Release It! introduced the circuit breaker pattern to mainstream software engineering: when a downstream dependency starts failing, stop calling it. The circuit trips open after a configurable threshold of failures. Subsequent requests fail fast without touching the network. After a cooldown period, a single test request checks whether the dependency has recovered. The pattern is borrowed from electrical engineering, where circuit breakers have been preventing cascading failures since the 1870s. In software, the circuit breaker is the difference between a single service degradation and a system-wide cascade. Bulkheads isolate failure domains so that one dying service cannot drain the connection pool shared by everything else (the term comes from shipbuilding: watertight compartments that keep a ship afloat when one section floods). Retry logic needs exponential backoff with jitter, or retries create thundering herds that flatten a service trying to recover. Timeouts must be set on every single network call. A service without timeouts is a service waiting for a hung dependency to take it down. The default timeout in most HTTP clients is either infinite or 30 seconds. Both are meaningless in a system where acceptable latency is measured in milliseconds.
The design principle that ties all of it together: graceful degradation. When a dependency fails, serve cached data. Serve stale data. Serve partial data. A product page with a price that is five minutes old is vastly better than a 500 error. Stale data beats no data. Partial results beat empty results. The goal is not perfection under failure; it is controlled imperfection that keeps the user experience functional while the system heals. Assume every network call will eventually fail. Design the failure mode you want rather than discovering the one you get.
The Architecture You Actually Need
The best preparation for a successful microservices architecture is a well-modularized monolith: clear domain boundaries, clean interfaces between modules, disciplined data ownership. A monolith with those properties is straightforward to decompose if and when decomposition becomes necessary. You already know where the seams are, because you drew them deliberately. And a monolith with those properties often does not need to be decomposed at all, because the discipline that makes decomposition easy is the same discipline that makes the monolith pleasant to work in. A well-structured monolith is better than a poorly-structured microservices architecture. Every single time.
If and when you do migrate, the Strangler Fig pattern remains the safest approach. Put an API gateway or a routing layer in front of the monolith. Extract one bounded context at a time, starting with the one that has the clearest boundaries and the most independent data. Route traffic to the new service incrementally: ten percent, then fifty, then a hundred. Keep the monolith running for everything else. This is slower than a clean-room rewrite and vastly more likely to succeed, because you are making small, reversible bets instead of one large, irreversible one. Fowler documented this pattern specifically because rewrites fail at rates that should make any engineering leader nervous. The Netscape rewrite. The Perl 6 rewrite. The second-system effect that Fred Brooks warned about in 1975.
The framework is not complicated. Is deploy contention your primary bottleneck? Can you draw the bounded contexts on a whiteboard, clearly and confidently, with stable boundaries proven in production? Do you have the engineering capacity to absorb the operational tax without that overhead consuming more engineering time than the deploy contention it replaces? If all three answers are yes, microservices may be the right call. Start with the Strangler Fig. Extract one service. Prove the boundary. Move slowly.
If any answer is no, invest in your monolith. Clean up the module boundaries. Establish clear interfaces between domains. Write the architecture decision record that explains why microservices are not the right call today and what conditions would change the decision.
The goal is not microservices. The goal is a system that ships reliably at the speed your organization needs. If you cannot draw the bounded contexts on a whiteboard, you are not ready. If you can draw them, check whether you actually need them. The answer, more often than the industry wants to admit, is no.