Architecture and distributed systems
Monolith vs Modular Monolith vs Microservices: A Decision Framework
Choose between a monolith, modular monolith, and microservices using a practical framework based on team structure, deployment, scale, data, and operational cost.
Architecture discussions often begin with the wrong question: “Should we use microservices?”
That question already assumes microservices are the destination and everything else is an earlier stage. They are not. A monolith, a modular monolith, and a microservices system solve different problems. Each can be a sensible final architecture. Each can also become an expensive mess.
I prefer a more useful question: what must be independent in this system, and what are we willing to pay for that independence?
If one small team owns the whole product, releases it together, and can scale it as one unit, a distributed system may add cost without removing a real constraint. If eight teams need to release different business areas every day, or one workload needs radically different scaling and reliability, a single deployment may become the constraint.
This guide offers a decision framework, not a universal winner. It looks at team ownership, deployment, scale, reliability, data, and operational ability. Those factors matter much more than whether an architecture looks modern on a diagram.
Start with the deployment boundary
People often use “monolith” to mean bad code. That is a mistake.
A monolith is mainly a deployment choice. The application runs and is released as one unit. Its internal code can still be clean, tested, and divided into well-designed modules. A microservices system can have the opposite problem: twenty deployments that are so tightly coupled they must always change together.
That gives us two separate questions:
- How is the code divided into responsibilities?
- How is the running system divided into independently deployed processes?
The first question is about logical boundaries. The second is about physical boundaries. Good architecture needs both, but they do not have to match on day one.
Microsoft's guide to common web application architectures makes the same useful distinction: a system can contain multiple libraries, components, or layers while still being deployed as a single application. That is the space where a modular monolith lives.
What each option actually means
Monolith
A monolith is one deployable application. It commonly has one codebase, one runtime process per instance, and one main database, although none of those details is an absolute rule.
In a simple monolith, presentation code, business logic, and data access may be separated only by folders or layers. One build produces one release. When traffic grows, the team usually creates more copies of the whole application behind a load balancer.
The main benefit is not that monoliths are old or easy. It is that calls inside the application are local, transactions can stay inside one database, and developers can trace a workflow without crossing a network.
Modular monolith
A modular monolith is also deployed as one unit, but its code is divided around business capabilities such as accounts, billing, catalogue, ordering, or reporting.
Each module has an explicit public interface. Other modules should not reach into its internal classes or update its tables directly. Communication may still happen through in-process function calls or internal events, so the application keeps the simplicity of one deployment without allowing every part to depend on every other part.
This is more than arranging folders nicely. A real module boundary must be enforceable. Dependency rules, tests, package visibility, separate schemas, or build checks should make accidental shortcuts difficult.
Microservices
A microservices architecture divides the application into independently deployable services. Each service owns a business capability, runs in its own process, and communicates over a network using APIs or messages.
Independent deployment is the important phrase. If changing the order service always requires coordinated changes to the customer, payment, and notification services, the system has several processes but little service autonomy.
Data ownership matters too. Microsoft's guidance on data sovereignty per microservice says that a microservice owns its domain logic and data. A service may use PostgreSQL while another uses a document store, but the deeper point is that other services do not bypass its interface and write to its tables.
The short comparison
| Concern | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | One unit | One unit | Many independent units |
| Internal boundaries | Often informal | Explicit and enforced | Process and API boundaries |
| Typical data model | Shared database | Shared server, preferably module-owned data | Service-owned data |
| Calls | In-process | In-process through module interfaces | Network calls and messages |
| Transactions | Simple across the application | Usually simple inside one deployment | Simple inside one service; difficult across services |
| Scaling | Scale the whole application | Scale the whole application | Scale selected services |
| Failure modes | Fewer moving parts; one bad component can affect all | Similar runtime risk, with better code isolation | Better potential isolation, but network and dependency failures appear |
| Operational cost | Lowest | Low to medium | Highest |
| Best fit | Small product or simple domain | Growing product that needs strong boundaries | Multiple autonomous teams or genuinely different runtime needs |
The table is deliberately unfair to slogans. Microservices gain deployment and scaling freedom, but pay for it with network failure, distributed data, more infrastructure, and more difficult debugging. A modular monolith gains strong code boundaries, but it cannot independently deploy or scale one module. A simple monolith keeps the shortest path to production, but can decay if nobody protects its structure.
The six-part decision framework
I use six questions to make the choice concrete. The first two usually carry the most weight.
1. How many teams need real autonomy?
Count teams, not developers.
Ten developers working as one product team do not automatically need ten services. They can often coordinate one deployment without much delay. On the other hand, six teams with separate roadmaps may lose days waiting for a shared release train or negotiating changes in the same code.
Microservices are valuable when a team can own a capability from code to production and release it without asking several other teams for permission. They are less valuable when ownership is split by technical layer—one frontend team, one backend team, and one database team—because every feature still crosses all groups.
Ask:
- Does one team understand and release the whole product?
- Are teams frequently blocking each other in the same repository or pipeline?
- Can one team own a service, its data, its alerts, and its incidents?
- Would a separate deployment remove a measured coordination problem?
If the answer to the last two questions is no, service boundaries may only turn human coordination into network coordination.
2. Do parts of the system need independent deployment?
Independent deployment is useful when different areas change at different speeds or carry different risk.
Perhaps the recommendation engine changes several times a day while the invoicing workflow changes once a month under strict review. Perhaps a payment integration needs an emergency rollback without touching the rest of the product. Those are meaningful reasons to separate a deployment.
“We might want to deploy independently later” is weaker. A modular monolith keeps code boundaries clear while the need becomes visible. Martin Fowler's monolith-first argument highlights a practical problem with splitting early: good service boundaries are hard to know before the team understands the domain, and moving behaviour across a network boundary is harder than moving it inside one process.
3. Are the scaling needs truly different?
Do not choose microservices merely because the product may become popular. A well-built monolith can run on several machines and handle substantial traffic.
The stronger signal is uneven demand. Video processing may need CPU-heavy workers. Search may need memory and a specialised index. A public catalogue may receive one hundred times more traffic than the administration area. Scaling the complete application for one hot path can waste money or create noisy-neighbour problems.
Before splitting, measure. A slow checkout caused by a missing database index will remain slow after it becomes a checkout service. A queue and worker process may isolate background work without requiring a complete microservices programme.
4. What consistency does the business require?
Data is where attractive service diagrams meet reality.
Inside one relational database, a transaction can update an order, reserve stock, and record a payment instruction together. Once three services own that data, there is no ordinary local transaction covering all three. The system needs messages, retries, idempotency, compensation, and a clear explanation of what users see while data is temporarily inconsistent.
Microsoft notes that service-owned data improves autonomy but makes cross-service consistency and queries harder. That is not a rare edge case. It affects search, reporting, permissions, deletion, and almost every workflow that spans capabilities.
If the team cannot yet describe how a duplicated event, delayed message, partial failure, or replay will behave, keeping the workflow in one transaction is often the safer design.
5. What failure isolation is required?
A monolith has a shared failure boundary. A memory leak, exhausted connection pool, or CPU-heavy operation can hurt the whole instance. Separate services can contain some failures and allow healthy areas to keep serving users.
But separation creates new failure paths. A local call that almost always succeeds becomes a network call that can be slow, fail halfway, or succeed after the caller has timed out. A service can be healthy while one of its dependencies is unavailable.
The Azure Architecture Center's microservices guidance is careful here: fault isolation works only when upstream services are designed to handle faults. Timeouts, bounded retries, circuit breaking, queues, idempotency, and degraded responses are part of the architecture. Drawing a box around a component does not isolate failure by itself.
Ask which failures must not spread, then design and test that behaviour. Sometimes the answer is a separate service. Sometimes a job queue, process boundary, database pool, or resource limit provides enough isolation at far lower cost.
6. Can the team operate a distributed system?
This question can veto all the others.
Microservices multiply deployable things. Every service needs configuration, secrets, health checks, security updates, logs, metrics, traces, alerts, ownership, capacity decisions, and a recovery path. Local development and integration testing become harder. An incident may cross five services and three queues.
Martin Fowler's microservice prerequisites include rapid provisioning, basic monitoring, rapid deployment, and close cooperation between development and operations. I would add reliable message handling, trace correlation, automated rollback, and an on-call model that knows who owns each service.
If deployments are still manual, logs live only on individual servers, and nobody has time to respond to service alerts, microservices will magnify those gaps. Improve the delivery and observability platform first. Those improvements also make a monolith safer.
When a monolith is the right choice
Choose a straightforward monolith when speed of learning matters more than independent change.
It is a strong fit when:
- One small team owns the product.
- The domain is still changing and its natural boundaries are unclear.
- Most features share the same release schedule and scaling profile.
- Workflows benefit from simple database transactions.
- The team needs to validate a market or replace a manual process quickly.
- Operational capacity is limited.
The danger is not the single deployment. The danger is allowing convenience to erase all structure. Keep business logic out of controllers, make dependencies point in a deliberate direction, and do not let every feature update every table.
A monolith is also not limited to one server. You can run multiple stateless instances, use a managed relational database, put slow jobs on a queue, and serve static assets through a CDN. Many scale problems can be solved without changing the basic application boundary.
When a modular monolith is the right choice
For many business applications, a modular monolith is my default.
It fits when the product is complex enough to need clear ownership, but the organisation does not need separate deployments. It lets the team discover whether “billing” or “fulfilment” is a stable business boundary without immediately paying for network calls and distributed transactions.
A useful modular monolith normally has these properties:
- Modules are named after business capabilities, not technical layers.
- Each module exposes a small public interface.
- Direct access to another module's internals is blocked.
- A module owns its rules and its part of the data model.
- Cross-module communication is visible and testable.
- The build checks dependency rules.
The database deserves special attention. Using one database server is fine. Allowing every module to join and update every table is not. Separate schemas, restricted database roles, repository ownership, or architecture tests can protect the boundary. You do not need to pretend the database is distributed, but you should know which module is allowed to change each piece of data.
The main limit remains: the whole application is released and normally scaled together. If that becomes a repeated, measurable constraint, a good module is a much safer extraction candidate than a random collection of classes.
When microservices are the right choice
Microservices earn their cost when independence has business value.
The case becomes strong when several of these conditions are true:
- Multiple teams own stable business capabilities end to end.
- Teams need to deploy on different schedules without coordination.
- Some workloads need very different scaling, security, availability, or technology.
- A failure in one capability must not take down another critical capability.
- The organisation already has strong automated delivery and observability.
- The domain and data ownership boundaries are understood.
- The expected benefit is large enough to fund ongoing platform and operational work.
Avoid making services too small. “One service per database table” produces chatty calls and weak ownership. A service should represent a useful business capability, not the smallest unit that can be placed in a container. AWS recommends decomposition by business capability, which tends to create more stable boundaries than splitting by technical function.
Also be honest about the shared database shortcut. Several services writing the same tables may be a temporary migration step, but it removes much of their independence. A schema change can still force coordinated releases, and no service fully owns the rules protecting the data.
A realistic SaaS example
Imagine a team building a subscription product for appointment booking.
At the start, four developers need to learn whether clinics will pay for it. They build one application with accounts, calendars, bookings, reminders, and billing. A monolith is a sensible choice: one local environment, one deployment, and simple transactions help the team move quickly.
The product succeeds. The code now has clear business areas, but shortcuts have spread. Reminder code reads booking tables directly. Billing knows about account internals. A change to clinic permissions breaks reporting.
The first useful move is not five services. It is a modular monolith. The team defines accounts, scheduling, notifications, and billing as modules. Each gets a public interface and owns its data changes. The deployment stays simple while the code becomes easier to reason about.
Later, reminders grow into email, SMS, and push notifications across several countries. Delivery volume is bursty, provider failures need retries, and a dedicated team owns the capability. Notifications now have a different scaling model, failure model, and release schedule. Extracting that module into a service may be justified.
Billing might follow because of stricter access, auditing, and provider integration. Scheduling may remain in the monolith because it shares transactions with bookings and has no independent scaling problem.
The result is not architectural failure or an unfinished migration. A system with a well-structured core and a few carefully chosen services can be exactly right.
Warning signs that the choice is wrong
Architecture should be revisited when evidence changes, not when a new trend arrives.
Your monolith may need stronger modules or selected extraction when:
- Unrelated changes repeatedly break each other.
- Teams wait on a shared release process every week.
- One workload dominates cost or harms the rest of the application.
- Ownership is unclear because everyone can change everything.
- Deploying a low-risk feature requires testing the entire product manually.
Your microservices design may be too distributed when:
- Most features require coordinated changes across several services.
- Services share a database or reach into each other's data.
- Local development needs a large cluster just to test one workflow.
- Engineers cannot trace a user request across the system.
- Small teams spend more time maintaining pipelines and contracts than delivering product work.
- Incidents bounce between teams because end-to-end ownership is missing.
In that situation, merging services can be a sound architecture decision. The goal is not to preserve the number of boxes. It is to make change safer and operation clearer.
How to change direction safely
Do not rewrite a working monolith into microservices in one large project. The technical risk is high, and product work usually cannot stop while the architecture catches up.
Begin by measuring the pain. Identify one capability with a clear owner and a reason to become independent. Clean its boundary inside the monolith first. Decide which data it owns, remove hidden callers, and create a narrow interface.
Then extract gradually. AWS's guide to the strangler fig pattern recommends routing selected behaviour to a new service while the existing system continues to handle the rest. This provides smaller migration steps and a practical rollback path.
For each extraction, answer these questions before moving traffic:
- Which source owns each record?
- How will old and new code communicate during the transition?
- What happens when a message is duplicated or delayed?
- How will requests time out and retry?
- How will operators trace one workflow across both systems?
- How can the team send traffic back if the extraction fails?
- What evidence will show that the split improved delivery, scale, or reliability?
The reverse path should also be available. If two services always change, deploy, and fail together, consider merging them. Architecture is allowed to become simpler.
My default decision rule
I start with the simplest deployment model that meets the known constraints, then design cleaner internal boundaries than the current team size appears to require.
For a new product with one team, that usually means a monolith. For a growing business application with several clear capabilities, it usually means a modular monolith. I choose microservices when specific parts need independent ownership, deployment, scaling, security, or failure isolation—and when the team can operate them well.
The rule is simple, but not simplistic:
Do not pay the distributed-systems cost until independence is valuable enough to cover it.
That cost is paid on every feature and every incident, not only during the first migration. The best architecture is therefore not the one with the most freedom in theory. It is the one that gives the organisation the freedom it will actually use, while keeping everything else understandable.
Frequently asked questions
Is a modular monolith just a well-organised monolith?
It is a monolith with enforceable business boundaries. Folder names alone are not enough. Modules need public interfaces, ownership of rules and data, and checks that prevent other modules from using their internals.
Are microservices always more scalable?
No. They allow parts of a system to scale independently, which helps when demand is uneven. A stateless monolith can also scale horizontally. Database design, caching, queues, and efficient code often matter before service count does.
How big should a microservice be?
There is no useful line-count rule. A service should own a cohesive business capability and be independently changeable. If two services constantly call each other and must release together, the boundary is probably too small or in the wrong place.
Can a modular monolith use one database?
Yes. One database keeps operations and transactions simpler. Protect module ownership with separate schemas, access rules, repositories, or automated dependency checks so that convenience does not turn into shared ownership of every table.
Should every new startup begin with a monolith?
Not every startup, but it is a strong default for one team exploring a new domain. A team may reasonably start with a few services when boundaries are already known, workloads are sharply different, or regulatory and isolation needs demand it. The reason should be concrete.
When should I extract the first service?
Extract when one well-defined module has a repeated need for independent deployment, scaling, ownership, security, or failure isolation. Clean the boundary inside the monolith first, then move it gradually with monitoring and a rollback path.
