A team of four builds a new product, reads a handful of engineering blog posts from companies with thousands of employees, and decides to structure their brand-new app as fifteen microservices from day one. Six months later, they’ve spent more engineering time on service discovery, distributed tracing, and figuring out why a single user action touches four different databases than they’ve spent on the actual product. This isn’t a rare cautionary tale. It’s one of the most common, well-documented failure patterns in modern software teams, and it happens because microservices articles tend to describe the benefits at Netflix’s scale without mentioning that those benefits only start applying once you’re actually operating at something close to that scale.
The honest version of this comparison isn’t “microservices are outdated” or “monoliths are always right.” It’s that microservices solve a specific organizational and scaling problem, and that problem doesn’t exist for most teams, most of the time. Understanding exactly what that problem is, and honestly assessing whether you have it, matters far more than picking whichever architecture sounds more sophisticated in an interview.
What a Monolith Actually Is
A monolith is a single, deployable application, one codebase, typically one shared database, built and shipped as a single unit even if internally it’s organized into distinct modules or layers. Deploying a monolith means deploying the whole thing at once. Scaling a monolith, in its simplest form, means running more copies of the entire application behind a load balancer, even if only one small part of it is actually under heavy load.
“Monolith” has picked up a slightly negative connotation over the past decade, partly as a side effect of microservices marketing, but a well-structured monolith with clear internal module boundaries is a completely legitimate, often superior architecture for a large share of real applications. The word describes a deployment unit, not a judgment about code quality. A poorly organized monolith with tangled, interdependent modules is a real problem, but the fix for that is better internal structure, not necessarily splitting into separate services.
What Microservices Actually Are
Microservices split an application into multiple independently deployable services, each typically owning its own data store, communicating with each other over the network (usually HTTP or a message queue) rather than through direct in-process function calls. Each service can be deployed, scaled, and even rewritten in a different technology stack independently of the others, at least in theory.
That independence is the entire point, and it’s genuinely valuable in the specific circumstances where you need it. A team owning the payments service can deploy a fix to that service without coordinating a release with the team owning the search service. A component with wildly different resource needs than the rest of the system, like a video transcoding service, can be scaled independently rather than forcing the entire application to scale together as one unit, even when most of it doesn’t need the extra capacity.
The Actual Problem Microservices Solve
Microservices were popularized by companies that had already grown past the point where a monolith’s core limitation, the fact that everyone working on it has to coordinate deploys, share a single codebase, and can’t scale one specific bottleneck independently of everything else, had become a genuine, daily bottleneck to shipping software with a large engineering organization. This is closely related to something called Conway’s Law: the structure of a software system tends to mirror the structure of the organization that built it. Once you have dozens of independent teams, forcing them all to work in and deploy from one shared monolith creates exactly the kind of coordination overhead microservices are designed to remove.
This is an organizational scaling problem as much as a technical one. A five-person team doesn’t have the coordination overhead microservices are solving for, because five people can coordinate a deploy by talking to each other directly. The same architectural pattern that removes friction for an eight-hundred-person engineering org adds friction for a five-person team, since that small team is now paying the network and operational cost of service boundaries without having the organizational scale problem those boundaries were designed to solve in the first place.
The Real Costs of Microservices
Every function call that used to be an in-process call inside a monolith becomes a network call between services, and network calls fail in ways in-process calls simply don’t: timeouts, partial failures, a service being temporarily unreachable while another one it depends on is still running fine. Handling this correctly requires genuine additional engineering work, retry logic, circuit breakers, sensible timeout policies, that a monolith never has to think about for calls happening within its own process.
Data consistency becomes a genuinely harder problem the moment each service owns its own database. A monolith with one shared database can wrap an operation touching multiple tables in a single ACID transaction, guaranteeing it either fully succeeds or fully rolls back. Once that same operation spans multiple services, each with its own database, you lose that guarantee entirely and have to build eventual consistency into your design deliberately, typically through patterns like the saga pattern, where a sequence of local transactions across services is coordinated with explicit compensating actions if a later step fails. This is real, non-trivial engineering complexity that doesn’t exist at all in a well-designed monolith.
Operational overhead multiplies directly with the number of services. Each service needs its own deployment pipeline, its own monitoring, its own logging, its own on-call awareness of what “normal” looks like for it specifically. Ten services means ten times the surface area for something to independently break, and debugging a single user-facing issue can mean tracing a request across several separate services and their logs rather than reading one application’s stack trace in one place. Distributed tracing tools exist specifically to make this manageable, but they’re an entire additional piece of infrastructure a monolith simply doesn’t need.
Testing gets harder too, in a way that’s easy to underestimate until you’re living with it. A true end-to-end test of a user flow that touches four separate services means either running all four services together in a test environment, which is its own significant infrastructure investment, or relying more heavily on service-level contract tests and accepting a real gap in genuine end-to-end coverage compared to what a monolith’s test suite can verify directly against one running application.
The Modular Monolith: The Option Most Comparisons Skip
Between “one tangled codebase with no internal structure” and “fifteen independently deployed services” sits an option that gets far less attention than it deserves: a modular monolith. This is a single deployable application, still one codebase and typically one shared database, but internally organized into clearly separated modules with deliberate, enforced boundaries between them, no reaching directly into another module’s internal data structures, communication through well-defined internal interfaces rather than whatever happens to be convenient.
This gets you a meaningful share of what people actually want from microservices, clear ownership boundaries, the ability for different people to work on different parts without constantly stepping on each other, code that’s easier to reason about in isolation, without paying the network latency, distributed data consistency, or operational multiplication cost of actually splitting into separate deployed services. And critically, if a specific module genuinely does need to become its own service later, because it needs independent scaling or an independent deployment cadence a specific team requires, extracting a well-bounded module from a modular monolith is a far more contained, lower-risk operation than untangling boundaries from a codebase that was never organized with any boundaries in mind to begin with.
Real Scenarios
Student project or portfolio piece
A monolith, without any real debate. There’s no team coordination problem to solve, no independent scaling need, and the added complexity of even a modular monolith’s internal boundaries is usually more structure than a project of this size benefits from. Keep the database schema clean and the code reasonably organized, and that’s the right amount of architecture here.
An early-stage startup building its first product
A monolith, ideally a modular one if the team has the discipline to maintain clear internal boundaries from the start. Domain boundaries are rarely well understood this early, and splitting into microservices based on a guess about where those boundaries belong is expensive to undo once the guess turns out wrong, which it very often does at this stage. Staying monolithic keeps that architectural decision genuinely reversible for longer.
A growing product with one specific, identified bottleneck
This is where extracting a single service, not restructuring the entire application into microservices, is the proportionate response. If one specific component (image processing, a search index, a notification system) has genuinely different scaling needs than the rest of the application, pulling just that one piece out into its own service solves the actual problem without taking on the full cost of a complete microservices migration for everything else.
A larger engineering organization with many independent teams
This is the scenario microservices were genuinely built for. Once dozens of engineers across several teams need to deploy independently without coordinating a shared release, and the organizational coordination cost of a shared monolith has become a real, measured bottleneck rather than a hypothetical one, the operational cost of microservices is being paid to solve a real, present problem rather than a speculative future one.
Comparison at a Glance
| Monolith | Modular Monolith | Microservices | |
|---|---|---|---|
| Deployment | One unit, all at once | One unit, all at once | Independent, per service |
| Data consistency | Simple, single-database ACID transactions | Simple, single-database ACID transactions | Eventual consistency, requires deliberate design |
| Operational overhead | Low, one thing to monitor and deploy | Low, same as monolith | Multiplies with service count |
| Team coordination needed | High for large teams sharing one codebase | Reduced, via internal boundaries | Low, teams deploy independently |
| Right team size | Small to mid-size | Small to mid-size, with growth room | Large, multiple independent teams |
Migrating Without a Risky Rewrite
If a genuine need to extract services eventually does arrive, the strangler fig pattern is the standard, lower-risk approach, named after a type of vine that gradually grows around and eventually replaces its host tree. Rather than a full rewrite, new functionality gets built as separate services from that point forward, while existing monolith functionality is gradually, incrementally extracted piece by piece, with the monolith and the new services running side by side throughout the transition, routing specific requests to whichever one currently owns that functionality.
This avoids the well-documented failure mode of a “big bang” rewrite, where an entire system gets rebuilt from scratch before anything new ships, often taking far longer than estimated and carrying real risk of introducing new bugs across the entire rewritten surface at once rather than validating each extracted piece independently as it’s completed. A strangler fig migration lets you validate each extracted service in production, with real traffic, before the next piece is touched, which is a meaningfully safer way to make this kind of architectural change than committing to a full rewrite upfront.
The infrastructure question underneath this migration is also worth answering deliberately rather than by default. Running several services, even a handful, means thinking about how they’re deployed and coordinated, which is exactly where the CI/CD pipeline and hosting decisions covered elsewhere on this site become directly relevant. This comparison of CI/CD tools covers the pipeline side of deploying multiple services independently, and this VPS comparison is worth revisiting once you’re hosting more than one deployable unit rather than a single application.
The Distributed Monolith Anti-Pattern
The worst of both worlds
A distributed monolith happens when a team splits an application into separate services without actually decoupling them properly, services that still have to be deployed together because they share a database, or because a change to one always requires a coordinated change to another. This configuration gets every downside of microservices, network latency, operational overhead, harder debugging across service boundaries, with none of the actual benefit, since the services still can’t be deployed or scaled independently in practice. It’s a genuinely common outcome of splitting into services before the actual domain boundaries were well understood, and it’s worse than either a clean monolith or genuinely independent microservices, because it combines the complexity of one with the coordination cost of the other.
Common Mistakes
Adopting microservices because of what a particular company’s engineering blog described, without an honest comparison between that company’s actual team size and organizational structure and your own, remains the single most common and most expensive mistake in this space. The specific benefits described in those posts are real, at the scale those companies operate at, and mapping onto a five-person team’s very different situation without adjustment is where the mismatch happens.
Splitting into services along boundaries guessed at before the domain was well understood is a related mistake with a longer-tail cost. Getting service boundaries wrong early means either living with an awkward, mismatched boundary indefinitely, or paying to re-split services later, which is a more disruptive, higher-risk change than getting the boundary right the first time would have been. A modular monolith, where internal boundaries are far cheaper to adjust than a live network boundary between separately deployed services, gives you meaningfully more room to get this right before it becomes an expensive decision to reverse.
Underestimating the data consistency implications of splitting a database across services until the problem shows up in production is another frequent one. It’s easy to plan a microservices migration around the API boundaries between services and genuinely not think through what happens to an operation that used to be one clean, atomic database transaction and now spans two services with two separate databases, until an actual production incident, an order marked complete while its corresponding payment record silently failed, forces the question.
And treating a microservices migration as a purely technical decision, disconnected from whether the team actually has the organizational structure and operational maturity (monitoring, on-call practices, deployment automation) to support it, sets the migration up to add real cost without the coordination benefit it’s meant to provide. Microservices without the matching organizational structure to actually take advantage of independent deployment just means more services to keep track of, without more independence in how they’re actually shipped.
FAQ
Is a monolith bad for a growing startup?
Not inherently. A well-organized monolith, particularly a modular one, can support a growing startup considerably longer than most people assume, and staying monolithic keeps the option to split later genuinely open, rather than locking in service boundaries before the domain is well understood.
How do I know if my team is big enough for microservices?
A useful signal is whether coordinating deploys across your current codebase has become a genuine, recurring bottleneck to shipping, not a hypothetical future concern. If a handful of people can still coordinate a release by talking to each other directly, the organizational problem microservices solve likely doesn’t exist yet.
Can I use microservices for just part of my application?
Yes, and this is usually the more proportionate approach compared to a full rewrite. Extracting one specific component with genuinely different scaling needs, while keeping the rest of the application as a monolith, solves the real, specific bottleneck without taking on the full operational cost of splitting everything.
What’s a distributed monolith and why is it worse than either option?
It’s a set of services split apart without proper decoupling, still requiring coordinated deployment or sharing a database. It combines microservices’ network and operational overhead with a monolith’s lack of independent deployability, getting the downsides of both without the benefits of either.
Should a new project ever start with microservices from day one?
Rarely, and mainly in cases where the domain boundaries are already extremely well understood from prior experience with a similar system, and multiple independent teams are already staffed and ready to own separate services immediately. For most new projects, domain boundaries are still being discovered, which makes starting monolithic the lower-risk choice.

