Monolith vs Microservices vs Modular Monolith
Monolith vs Microservices vs Modular Monolith
Microservices promise independence. But what do you give up to get it?
Build Voice Agents That Sound Natural Across Every Turn
Presented by Deepgram
Traditional text-to-speech models generate speech one response at a time, often losing conversational context. Deepgram Flux TTS maintains speech context across turns to keep tone, pacing, and delivery consistent, while supporting real-time streaming and interruptions. Deliver more responsive customer experiences with less speech orchestration work.
Monolith vs Microservices vs Modular Monolith
What’s worse: a release that breaks parts of the system you never touched, or an architecture so distributed that tracing one bug requires jumping across half a dozen services?
At first, that sounds like a choice between a monolith and microservices.
But the more useful question is: Do your module boundaries also need to become deployment boundaries?
Microservices separate parts of the system into independently deployable services. That can give teams more control over deployment, scaling, ownership, and failure isolation.
But that independence comes with a cost.
Local function calls become network requests. Database transactions can become distributed workflows. And one application becomes many services that need to be deployed, monitored, secured, and debugged.
A modular monolith keeps strong boundaries between parts of the application while still deploying them together.
So the decision is more about which parts of your system genuinely need to evolve independently, and whether separating them across the network is worth the added complexity.
“Monolith” describes deployment
A monolith is an application that is built and deployed as one unit.
That tells you how the system ships. It doesn’t tell you whether the code inside it is well structured.
A monolith can be tightly coupled, with every feature reaching into everything else. Or it can have clean modules, clear layers, and well-defined boundaries between domains.
What makes it a monolith is simply that those parts still run and deploy together.
That brings some useful advantages:
Local communication → Components can call each other directly without going across the network.
Simpler debugging → You can often trace a request through one application.
Straightforward transactions → Multiple database operations can participate in the same local ACID transaction.
Simpler deployment → You usually build, test, and release one application.
The main tradeoff is deployment coupling.
A small billing fix and an unrelated signup change may still ship in the same release. As the system and team grow, more changes share the same deployment schedule and failure boundary.
That can become painful when different teams want to move at different speeds.
But deployment coupling alone doesn’t mean the system needs to be split into microservices.
Sometimes the problem isn’t the monolith
As an application grows, another problem can appear: the internal boundaries start to break down.
Orders reaches directly into Payments. Inventory updates Catalog tables. Teams reuse shared classes because it’s convenient. Business logic slowly spreads across domains.
Eventually, a small change in one area starts affecting several others.
Now changing one feature requires understanding a large part of the application.
It’s easy to blame the monolith at this point. But the deeper problem is often coupling.
The system may still be deployed as one unit, but internally, its parts are no longer meaningfully separated. Microservices can enforce stronger boundaries by putting those parts into separate services.
But you don’t need a network boundary to create a strong software boundary.
A modular monolith separates responsibility without separating runtime
Imagine an e-commerce backend split into a few business modules:
Catalog → Owns products, descriptions, and pricing.
Inventory → Owns stock levels and reservations.
Ordering → Owns carts and order workflows.
Payments → Owns payment processing and payment state.
Shipping → Owns fulfillment and delivery.
The application still deploys as one unit.
But internally, each module owns its logic and exposes only a small public interface.
For example, Ordering shouldn’t update Inventory tables directly just because both modules use the same database. Instead, it asks Inventory to reserve stock through an interface that Inventory controls.
That’s the important distinction.
If the modules are only folders, developers can still reach across those boundaries whenever it’s convenient. The code may look organized while the underlying system remains tightly coupled.
A modular monolith becomes useful when those boundaries are actually enforced.
You now get logical separation without physical separation.
Ordering can still call Inventory in-process. The application can still deploy as one unit. And multiple operations can still participate in the same local database transaction when needed.
Each module owns a clear responsibility, without forcing that responsibility into a separate runtime.
For many systems, that gives you enough isolation without taking on the operational cost of microservices.
The next question is what happens when a module needs more independence than an internal boundary can provide.
Microservices move the boundary across the network
Microservices start with the same idea as a modular monolith: separate the system into clear business capabilities.
The difference is that those boundaries now exist across the network.
Ordering and Inventory are no longer modules inside the same application. They become separate services that can deploy independently, usually communicate through APIs or messages, and typically own their own data.
That gives you a stronger form of independence:
Independent deployment → Payments can release without rebuilding and deploying Catalog.
Independent scaling → A resource-heavy service can add capacity without scaling the entire system.
Technology independence → A service can use a different language, database, or runtime when there’s a good reason.
Stronger fault isolation → A failing service may degrade independently instead of affecting the entire process.
Team autonomy → Different teams can own services and release them on different schedules.
The key benefit is having parts of the system that can change independently.
Scaling makes this easier to see.
Let’s take an example where recommendations consumes most of your compute, while account management uses very little.
A monolith can still scale horizontally. But every new instance contains both capabilities, even if only one needs more capacity.
If recommendations becomes its own service, you can scale it on its own.
That matters even more when workloads need different infrastructure. One service may need GPUs, another may need large amounts of memory, while the rest runs comfortably on standard instances.
That’s a good reason to introduce a service boundary.
A monolith can also run across many instances behind a load balancer. If most parts of the system grow at roughly the same rate, scaling them together may be simpler and perfectly reasonable.
The main question is: Do different parts of the system need to scale differently?
You can apply the same test to deployment, ownership, and availability.
When different parts genuinely need different behavior, moving the boundary into a separate service can make sense.
But once that boundary crosses the network, the failure model changes too.
The cost of microservices
Inside a modular monolith, one module calling another is usually a simple in-process operation.
Move that interaction into separate services and the failure model changes.
A request can time out. A service can be unavailable. A retry can run twice. Messages can arrive late. Latency can build across several downstream calls.
Debugging changes too. Instead of tracing one request through one application, you may need logs and distributed traces across several services.
The same applies to data.
For example, placing an order requires:
Creating the order.
Reserving inventory.
Recording payment.
Inside one application and database, those changes may fit inside a single ACID transaction.
Split them across separate services with separate databases, and that local transaction disappears.
You may now need a saga, where each service commits independently and failures trigger compensating actions. If inventory is reserved but payment fails, another step has to release that stock.
That introduces more moving parts:
Retries → Failed operations may need to be attempted again safely.
Idempotency → Repeating the same request shouldn’t create duplicate effects.
Eventual consistency → Different services may temporarily disagree about state.
Compensation logic → Completed steps may need to be undone after later failures.
Observability → Logs and traces must connect work happening across services.
And that’s before the operational overhead.
More services also mean more deployment pipelines, health checks, secrets, configuration, routing, monitoring, alerts, and rollback procedures.
Microservices don’t remove complexity. They trade some local simplicity for runtime independence.
That trade can be worthwhile when teams genuinely need to deploy, scale, or operate parts of the system independently.
But more services aren’t automatically better.
The number of services matters less than whether those services actually need to change independently.
If several parts of the system constantly deploy together, update the same business state, and fail together, separating them may add complexity without creating much independence.
The worst outcome is a distributed monolith
The worst tradeoff is taking on distributed-system complexity without gaining real independence.
You can split one application into twenty services. But if those services still need to change together, deploy together, or depend heavily on each other at runtime, they still behave like one tightly coupled system.
That’s a distributed monolith: multiple services physically separated across the network, but still coupled like a monolith.
Common signs include:
Coordinated deployments → Changing one service requires updating several others.
Shared data dependencies → Services reach into the same schemas or depend on each other’s internal data.
Long synchronous chains → One request needs several services to succeed in sequence.
Shared libraries everywhere → One library change forces many services to be rebuilt or redeployed.
Cascading failures → One unhealthy service can break several downstream workflows.
So you pay for network failures, distributed tracing, operational overhead, and multiple deployments while keeping much of the coordination cost you were trying to remove.
Ultimately, the boundaries still determine how loosely coupled a system is.
Choosing the right boundary
A monolith, modular monolith, and microservices architecture can contain the same business capabilities.
What changes is where the boundaries are enforced.
Monolith → Boundaries mostly depend on code structure and team discipline.
Modular monolith → Boundaries are explicit and enforced, but still live inside one deployment.
Microservices → Selected boundaries move into separate services, with their own runtime, data, deployment, and network communication.
Stronger boundaries can give you more independence.
But they also remove conveniences.
That’s why these architectures are not levels of maturity. A successful monolith doesn’t automatically need to become microservices.
A common evolution might look like: Monolith → Modular Monolith → Selective Microservices
The key word is selective.
Most functionality can stay inside a modular monolith while a few capabilities move out because they genuinely need different deployment, scaling, availability, technology, or ownership.
So start with the constraint.
If you need cleaner separation in the code, strengthen the module boundary.
If you need independent runtime behavior, consider moving that boundary across the network.
👋 If you found this useful → Like + Restack to help others learn system design.
Subscribe to get high-signal and clear system design deep dives, case studies, and breakdowns straight to your inbox:
© 2026 Level Up Coding
548 Market Street PMB 72296, San Francisco, CA 94104