A modular monolith is the better default for most growing codebases. Microservices become the better choice when separate teams need genuine ownership of independently deployable capabilities—and the organization can absorb the operational and consistency costs that come with distribution.
That is the trade-off usually missing from the modular monolith vs microservices debate. It is not simply one deployable versus many, or small traffic versus large traffic. It is a choice about who can change what, who owns the data, how independently teams must release, and whether the organization can operate the resulting system.
Imagine a product team that splits a growing application into a dozen services. The intention is to let teams move independently. Instead, a small feature now crosses four services, local development requires several infrastructure dependencies, and a routine release needs coordinated API changes. The team has more deployment units but less practical independence. This is a hypothetical scenario, not a report of a particular company. It illustrates the central failure mode: distribution without autonomy.
The three architectures are not synonyms for code size#
A monolith is an application delivered as one deployable unit. The term says little about internal quality: a monolith can have coherent modules and explicit interfaces, or it can be a tangled codebase where any part reaches into any other.
A modular monolith keeps one deployment boundary while enforcing internal boundaries. Modules own domain behavior, expose narrow interfaces, and restrict access to implementation details. They may share a process and database, but that should not grant every module unrestricted access to every table or internal package.
Microservices split an application into independently deployable services, each responsible for a cohesive business capability. A service owns its logic and data, communicates through explicit contracts, and can change without synchronized releases of its neighbors. Microsoft describes data ownership and an autonomous lifecycle as central principles (Data sovereignty per microservice).
The important distinction is not whether the repository contains one folder or several, or whether the runtime uses containers. It is where the system draws its boundaries and what those boundaries allow teams to do.
| Dimension | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | One application unit | One application unit | Multiple independently deployable services |
| Internal boundaries | May be weak or strong | Explicit, enforced module boundaries | Service/API boundaries across processes |
| Data ownership | Often broadly shared | Module ownership can be logical within shared infrastructure | Each service owns its data; other services use contracts |
| Communication | In-process calls | In-process module APIs or events | Network calls and/or asynchronous messages |
| Transactions | Usually local ACID transactions | Local transactions can cross modules if designed that way | Cross-service workflows require distributed coordination or eventual consistency |
| Scaling | Commonly scale the application unit | Commonly scale the application unit | Scale individual services where workload justifies it |
| Operations | One main runtime unit, plus dependencies | Similar deployment footprint to a monolith | Service discovery, telemetry, deployment, networking, and failure handling across units |
A modular monolith establishes domain boundaries without paying for distributed execution before it creates value.
The decision is organizational before it is computational#
Team boundaries and Conway's Law#
Melvin Conway's 1968 paper, “How Do Committees Invent?”, argues that system designs tend to reflect the communication structures of the organizations that create them. The practical implication is uncomfortable: a service map that does not match real ownership can create more coordination rather than less.
If one team owns the product end to end, splitting it into many services may introduce service-to-service coordination without creating independent decision-making. If several teams own distinct business capabilities, a well-designed service boundary can give each team a clearer area to change, deploy, and operate.
Team count is not a formula: five teams do not automatically require five services. Examine which teams change the same domain rules, negotiate dependencies frequently, or can evolve with few cross-team decisions. A boundary should reduce necessary communication, not hide it behind HTTP.
Rate of change and deployment independence#
Microservices help when components need different release schedules. But one deployable does not prevent continuous delivery: a modular monolith can release frequently when its build, tests, migrations, and rollout process support it.
Ask: what change is blocked by the shared deployment boundary? “The monolith feels old-fashioned” is not a reason. Repeated delays caused by unrelated changes or incompatible release schedules are stronger evidence—if module boundaries cannot solve them.
Martin Fowler's analysis of microservice trade-offs identifies independent deployment and stronger module boundaries as benefits, while warning that service dependencies can shift complexity into inter-service coordination and operations (Microservice Trade-Offs).
Workload differences and scaling#
Different scaling profiles can justify separate services: a background processor, for example, may need different resources from a latency-sensitive API. But “we expect scale” is not enough. Identify the bottleneck and compare the cost of scaling one capability with the cost of scaling the application as a whole.
Consistency and data ownership#
A modular monolith can often update related records in one database transaction. Microservices make data ownership explicit but turn cross-service consistency into a design problem. A workflow that updates inventory, payment, and order state may need a saga: local transactions coordinated with compensating actions when a later step fails (Microsoft's Saga pattern).
Eventual consistency can be appropriate, but retries, idempotency, reconciliation, and user messaging must account for intermediate states. If the domain needs frequent atomic updates across proposed boundaries, those boundaries may be wrong—or distribution may be premature.
Operational maturity#
Microservices require more than containers: deployment automation, health signals, centralized logs, distributed tracing, alert ownership, configuration and secrets management, incident response, and workable local development. Microsoft highlights distributed tracing as a key part of observability across service boundaries (Microservices architecture style). If a team cannot trace a request or safely roll back one service, the operational model is not ready.
What microservices actually cost#
The most visible cost is network communication. A remote call can time out, fail, arrive late, or return after the caller's deadline. Synchronous chains amplify latency and make one capability depend on several others. Teams need timeouts, bounded retries, circuit breakers where appropriate, idempotency, and explicit fallback behavior; infrastructure cannot eliminate these design responsibilities.
The second cost is distributed data. Queries that once joined tables may now require APIs, read models, or asynchronous projections. Cross-service workflows need explicit consistency semantics. A boundary drawn around a code folder rather than a business capability can turn simple reads and writes into coordination protocols.
The third cost is contract evolution. APIs are compatibility promises: teams must manage schema changes, deprecation, and rollout order. Backward-compatible contracts enable independent deployment; breaking changes reintroduce coordination.
The fourth cost is observability and local development. Cross-service bugs require correlated traces and logs; developers may need dependency stacks, stubs, or shared environments. A slow, fragile local workflow can erase the autonomy services were meant to provide.
These costs compound: poor boundaries increase calls, failures, and tracing complexity; shared data weakens independent deployment; coordinated releases undermine the split. Microservices make coupling explicit—and more expensive when boundaries are wrong.
What a modular monolith still costs#
A modular monolith's main failure mode is boundary erosion: developers import another module's internals, write to its tables, or create circular dependencies. The module map then becomes documentation rather than architecture.
The answer is enforcement, not naming conventions alone. Expose narrow module APIs, keep internals inaccessible, prohibit dependency cycles, and add automated architecture tests. Spring Modulith, for example, can verify that module dependencies are acyclic and that access to other modules goes through API packages (module verification). The same principle can be implemented with architecture tests and package rules in other ecosystems.
The shared deployment is another constraint: modules ship together, and one module's resource demand may force the whole application to scale. These costs matter when they materially restrict delivery, reliability, or economics.
Shared database access also needs discipline. Logical ownership is weaker than physical isolation; if every module can query every table, coupling returns. Use module-owned schemas or repositories where appropriate, and make cross-module reads explicit.
The distributed monolith: the worst of both#
A distributed monolith has multiple services but lacks the autonomy that justifies them. Services call each other synchronously in long chains, share databases or schemas, require coordinated releases, and cannot be developed or operated independently.
It pays the network, deployment, observability, and local-development costs of distribution while preserving the tight coupling of a monolith. The system is physically distributed but organizationally and behaviorally entangled.
Warning signs include:
- A routine feature requires changes and releases in several services.
- Services read or write another service's database directly.
- A single user request depends on a long synchronous chain of services.
- Teams cannot deploy a service without coordinating with its consumers.
- Local development requires a large, brittle stack of services to be running together.
- Nobody can clearly identify ownership for a service's data, alerts, or on-call responsibility.
A service count is not evidence of microservice autonomy. If the units cannot change independently, the architecture has not delivered the property it was supposed to buy.
Signs you are waiting too long#
A modular monolith can become a bottleneck when a cohesive capability has a different scaling profile, security boundary, availability requirement, or release cadence. Extraction may also be justified when team ownership is persistently blocked or one module's failures threaten unrelated capabilities.
Do not use “the codebase is large” as the trigger. Look for a specific constraint with a measurable consequence: releases repeatedly blocked by unrelated work, an independently scaling workload, a data boundary that must be isolated, or a team boundary that cannot be represented cleanly inside the shared application.
Extraction should remove the constraint. Moving classes to another repository and adding an API call is not enough.
Keep the option open: modularize before you distribute#
A practical path is to establish boundaries while the system is still one deployable unit, then extract only where evidence points to a real need.
- Model business capabilities. Identify cohesive areas of domain behavior and the data each area owns. Avoid drawing modules solely around technical layers such as controllers, services, and repositories.
- Define module interfaces. Expose use-case-oriented APIs or domain events. Keep implementation details private and make dependencies intentional.
- Enforce the boundaries. Add automated checks for cycles, forbidden imports, direct cross-module database access, and unintended dependency directions.
- Make ownership explicit. Assign a team or clear owner to each capability, including its data, tests, operational concerns, and change decisions.
- Measure friction. Track which releases require coordination, which modules change together, where resource pressure occurs, and which cross-module workflows are difficult to maintain.
- Extract one capability when a constraint justifies it. Choose a boundary with high cohesion and few synchronous dependencies. Give the new service ownership of its data and a contract that can evolve independently.
This is not a commitment to remain a monolith. It delays distribution until the domain, ownership model, and operational requirements are clearer—a position related to Fowler's “Monolith First” argument (Monolith First).
A decision checklist#
Choose a modular monolith when most of these statements are true:
- One team, or a small group of closely collaborating teams, owns most of the application.
- Capabilities frequently participate in the same transactions or business workflows.
- A shared release process is not a material delivery bottleneck.
- Workloads have broadly similar scaling and runtime requirements.
- The team does not yet have mature distributed tracing, deployment automation, and service ownership.
- Internal module boundaries can provide the isolation the codebase currently lacks.
Consider microservices when several of these statements are demonstrably true:
- Distinct teams own cohesive business capabilities and need independent decision-making.
- Independent deployment is a recurring requirement, not an architectural aspiration.
- One or more capabilities have materially different scaling, security, availability, or technology needs.
- Data ownership can be separated without constant cross-service transactions.
- Teams can operate their services, respond to alerts, trace distributed requests, and manage compatibility.
- The benefits of autonomy outweigh the ongoing cost of network communication and distributed operations.
If the evidence is mixed, do not force a binary decision. Keep a modular monolith, strengthen its boundaries, and revisit the decision when a specific constraint appears.
The architecture should follow the organization—and the evidence#
The modular monolith vs microservices decision is not a maturity badge; it is a choice about the location and cost of boundaries. A monolith keeps communication in-process and releases together. Microservices move boundaries onto the network and provide deployment and data autonomy when those boundaries are real. A modular monolith lets a growing codebase establish boundaries before distributing them.
The default recommendation is therefore straightforward: start with a modular monolith, enforce its boundaries, and extract a service only when a specific team, deployment, data, security, or scaling constraint makes the trade-off worthwhile. Do not split because the system is popular, the codebase is large, or microservices appear more sophisticated. Split when the organization can name the autonomy it needs and can operate the complexity it is choosing.
Use the Architecture Patterns Reference as the main companion when evaluating structural options. For a related way to quantify workload and capacity constraints before deciding whether separate scaling is justified, see the System Design Cheat Sheet.
References#
- Microservice Trade-Offs — Martin Fowler
- Monolith First — Martin Fowler
- Data Sovereignty per Microservice — Microsoft Learn
- Saga Design Pattern — Microsoft Learn
- Microservices Architecture Style — Microsoft Learn
- Verifying Application Module Structure — Spring Modulith



