There's a pattern I've seen play out more times than I can count. A team starts a new project. Someone — usually the most senior developer, or the one who just attended a conference — proposes microservices. The architecture diagram looks impressive. Everyone nods. Six months later the team is debugging a payment failure that spans four services, three message queues, and two databases, and nobody can tell you what actually went wrong.
The instinct to reach for microservices isn't irrational. The architecture works extremely well — at the right scale, with the right team, for the right problem. The mistake is treating it as the default choice rather than the result of specific, earned constraints.
What a monolith actually is
The word "monolith" has become a slur in software circles. It conjures images of spaghetti code, 100,000-line files, and deployments that require a prayer. That's not what a monolith is.
A monolith is a single deployable unit. One codebase, one process, one database. That's it. Whether it's clean or a mess is an entirely separate question about how the code is structured internally — not about the deployment model.
A well-structured monolith can have clear domain boundaries, isolated modules, rigorous separation of concerns, and a test suite that runs in under a minute. The fact that it deploys as one unit doesn't make it fragile or hard to change. Those properties come from the quality of the code, not the topology of the services.
What microservices actually solve
Microservices were popularized by teams at companies like Netflix and Amazon. Both were operating at enormous scale with dozens of engineering teams working on the same system simultaneously. The architecture solved real problems they had:
- Independent deployability: Team A can release their service without coordinating with Team B.
- Independent scalability: The recommendation engine needs 10× the compute of the billing service. Scale them separately.
- Fault isolation: A crash in the notification service doesn't bring down the payment flow.
- Technology flexibility: The ML team uses Python. The transaction engine uses Go. Both can coexist.
These are legitimate benefits. They're also benefits you don't need until you're dealing with the problems that require them.
If you have one team, you can coordinate deployments. If you're serving a few hundred requests per second, you don't need to scale services independently — your monolith can handle it on a single server. If one crash bringing down the whole application is unacceptable, you have a reliability problem that better infrastructure and error handling will solve faster than splitting your codebase.
The hidden costs you pay before you see the benefits
Microservices are not free. Every service boundary you introduce creates a coordination problem.
Network becomes your bug surface. Function calls are fast and reliable. HTTP requests between services are slow and unreliable. Every inter-service call is a potential timeout, a potential 500, a potential data inconsistency. Distributed transactions are notoriously hard to reason about, and eventual consistency creates bugs that are nearly impossible to reproduce in development.
Observability becomes a prerequisite. When a request fails in a monolith, you look at one log. When a request fails across eight services, you need distributed tracing, correlated request IDs, a centralized log aggregation system, and enough context in every log line to reconstruct what happened. Getting this right is a meaningful engineering investment before you can even start debugging production issues effectively.
Local development becomes painful. Running a monolith locally means starting one process. Running a microservices architecture locally means spinning up a dozen services, often with Docker Compose, with non-trivial configuration to get them talking to each other. Developers spend time on infrastructure instead of product.
Deployment complexity compounds. With a monolith, you deploy one thing. With microservices, you deploy many things, manage their versions, handle schema migrations across service boundaries, and coordinate releases when services have interdependencies.
None of this is insurmountable. But it's work that must be done before the architecture pays off. Teams that adopt microservices early spend that time on infrastructure that doesn't ship product.
The actual decision framework
The question isn't "monolith or microservices?" The question is: what constraints do you actually have right now?
Start with a monolith if:
- You're building something new and the domain isn't fully understood yet
- You have fewer than three or four teams working on the system
- You don't have different scaling requirements for different parts of the system
- You don't have independent release cadences across product areas
- You don't have the observability infrastructure to operate distributed systems safely
Consider splitting services when:
- Specific parts of the system have dramatically different scaling needs
- Teams are actively blocking each other on deployments
- A component has genuinely independent reliability requirements
- You have a clear, stable service boundary — not a guess about where one might exist
That last point matters more than people admit. Microservices work when you cut along the right seams. Cut along the wrong seams and you end up with distributed coupling — services that need to coordinate on every operation because the boundaries don't match how the domain actually works. This is harder to fix than a messy monolith.
The middle path worth considering
If you're working on a monolith and feeling the friction, the answer usually isn't to split into services. It's to impose better structure inside the monolith first.
A modular monolith applies the same domain boundary thinking as microservices — clear modules with explicit interfaces, no circular dependencies, enforced separation between concerns — but keeps a single deployable unit. Each module owns its data and exposes a defined API to the rest of the system.
This gives you most of the maintainability benefits of service-oriented thinking without the operational overhead. When you do eventually need to extract a service — because you have a specific, proven need to — the boundaries are already there. You're splitting along seams you've already validated, not guessing.
What I've learned building both
I've designed systems that started as tight monoliths and stayed that way profitably for years. I've also helped teams untangle microservice architectures that were extracted too early and spent more engineering time on infrastructure than on the product itself.
The teams that made the right call weren't the ones with the most sophisticated architecture. They were the ones who understood their actual constraints — team size, scale, domain complexity, deployment cadence — and matched their architecture to those constraints instead of to industry trends.
Start simple. Add complexity when you have a specific reason to. And when someone draws an impressive architecture diagram, ask one question: what problem does this solve that we actually have right now?
If the answer is "it's more scalable," ask how many users you're expecting in the next 12 months. The answer usually settles the debate.