Monolith vs Microservices: The Honest Tradeoffs


Microservices became the default architectural choice for new systems around 2015-2018. The reasoning was usually: microservices scale better, allow independent deployments, let teams work independently, and are what Netflix/Google/Amazon do.

Some of this is true. Most of it is incomplete. The Netflix/Amazon/Google argument is particularly misleading - those companies adopted microservices after they hit real scaling problems that monoliths couldn’t solve, not as a starting point.

The honest version of this tradeoff is more nuanced than most architectural discussions admit.

What a Monolith Actually Is

A monolith is a system where all components run in the same process and are deployed together. This is the natural starting point for any new system. The components can have good internal architecture - clear modules, strong interfaces, separated concerns - while still being deployed as a single unit.

“Monolith” has acquired negative connotations that it doesn’t deserve. A well-structured monolith is often the right architecture for a long time. The problems people associate with monoliths (“big ball of mud,” hard to understand, impossible to change) are not inherent properties of the monolith architecture. They’re symptoms of poor internal structure that would cause problems in a microservices system too - just different problems, more expensively.

What Microservices Actually Cost

The benefits of microservices are frequently discussed. The costs less so.

Distributed systems complexity - when your system is distributed, you now have to handle network failures, partial failures, latency, and inconsistency. A function call that couldn’t fail now has a network boundary and can fail in many ways: timeout, connection refused, response not received, response received but invalid. Every service interaction requires retry logic, circuit breakers, and timeout handling.

Data consistency - a monolith can use database transactions to maintain consistency across operations. In microservices, operations that span services can’t use a single transaction. You’re choosing between eventual consistency, sagas (long-running transactions with compensating actions), or 2-phase commit (rarely practical). None of these are as simple as a database transaction.

Operational overhead - you now have N services to deploy, monitor, and debug instead of one. Each service needs its own deployment pipeline, health checks, logging, tracing, and on-call runbook. For a small team, this overhead is significant.

Local development complexity - running the full system locally means running 10 services instead of 1. Developers spend time managing Docker Compose files and debugging “service A can’t reach service B” issues that don’t exist in a monolith.

Distributed debugging - when something goes wrong, the failure might span three services. Without distributed tracing, finding the root cause requires correlating logs across multiple systems. Even with tracing, debugging distributed failures is harder than debugging in-process failures.

What Microservices Actually Solve

When microservices are the right answer, it’s usually for one of these specific reasons:

Independent scaling: part of your system has dramatically different load characteristics than the rest. Your image processing service needs 20 CPUs and your API service needs 2. A monolith would require scaling the whole thing together.

Organizational scaling: your team has grown large enough that the coordination cost of working in a single codebase is significant. Conway’s Law applies: systems reflect the communication structures of the organizations that build them. Microservices with clear ownership boundaries can reduce coordination overhead when you have many teams.

Independent deployment: teams need to deploy their components independently without coordinating with other teams. In a monolith, a deployment includes everything, and a bug in one area can block deployments from another area.

Technology diversity: different components genuinely benefit from different technology stacks. ML models in Python, real-time processing in Go, legacy integration in Java. In a monolith, you’re mostly committed to one stack.

Notice what’s not on this list: “we’re building a new product.” The benefits of microservices are largely about managing scale - organizational or technical - that you don’t have yet at the beginning of a project.

The Migration Path

The practical pattern that works well:

  1. Start with a well-structured monolith. Invest heavily in internal modularity - clear module boundaries, dependency rules, good interfaces between components.

  2. When you hit a specific problem that the monolith can’t solve (a component that needs independent scaling, a team ownership conflict, a technology mismatch), extract that component as a service.

  3. Repeat as needed, guided by actual problems rather than anticipated ones.

This is the “modular monolith” approach. Martin Fowler calls it “don’t start with microservices.” The internal structure matters: a modular monolith where each component has clear interfaces is much easier to extract into services later than an entangled monolith where everything touches everything.

When You Inherit the Decision

If you’re joining a system already built as microservices, or inheriting one that’s become a distributed monolith (all the complexity of microservices, none of the independence), the calculus is different. Collapsing services back into a monolith is possible but organizationally difficult.

For inherited microservices systems, the focus should be on investment at the boundaries: clear service contracts, good tracing and observability, robust retry and circuit breaking patterns, and clear ownership. These reduce the cost of the architecture you have.

The Meta-Point

Architectural decisions made at the beginning of a project carry enormous inertia. Starting with microservices because “that’s the modern way” creates years of operational overhead for problems you might never have.

The companies that succeed with microservices typically started simpler and migrated when specific pain drove specific changes. The companies that struggle with microservices typically adopted them early because they seemed like best practice.

Ask “what specific problem does this solve for us right now?” before adopting any architecture. If the answer is “it scales better” - does your current system actually have a scaling problem? If the answer is “teams can work independently” - do you have enough teams that this is actually a bottleneck?

Good architecture solves real problems in front of you. Not problems you expect to have if things go well.



Read more