Lesson 53 of 129
Monolith, Modular Monolith, SOA, and Microservices
An application architecture is defined by two properties: how the code is divided into modules, and how those modules are packaged into deployment units (the artifacts that are built, released, and scaled as one). Monoliths, modular monoliths, service-oriented architecture (SOA), and microservices are points on this spectrum. They differ in where module boundaries are enforced and whether a boundary is a function call or a network call.
Monolith
A monolith is an application built and deployed as a single unit. All features run in one process (or one set of identical processes behind a load balancer) and usually share one database.
Properties:
- One codebase, one build, one release.
- Calls between components are in-process function calls: nanoseconds, no serialization, no partial failure.
- A transaction can span any tables, so ACID guarantees are available everywhere.
- The whole application scales together. To handle more load on one feature, all features are replicated.
- One language and runtime for the whole application.
A monolith is not inherently unstructured. A monolith without enforced internal boundaries is often called a big ball of mud: any module can call or query any other, so a change in one place can break code far away.
Modular monolith
A modular monolith is a single deployment unit whose code is divided into modules with explicit boundaries. Each module exposes a narrow public interface and hides its internals. Other modules may use only the interface.
Mechanisms that enforce the boundaries:
- Language-level visibility (Java modules and
publictypes, package-private classes). - Separate build modules with declared dependencies, checked in CI.
- Each module owns its tables. Other modules do not query them directly; they call the owner's interface.
- Communication through interfaces or in-process events, not through shared mutable state.
The deployment stays simple (one artifact, in-process calls, local transactions) while coupling is controlled. A well-bounded module can later be extracted into a service with little redesign, because its callers already depend only on an interface.
Service-oriented architecture
SOA is an architectural style, prominent in the 2000s enterprise world, in which business functions are exposed as reusable services with published contracts, typically over SOAP/WSDL web services and coordinated through an enterprise service bus (ESB).
Typical SOA characteristics:
- Coarse-grained services shared across the whole organization (for example, "Customer" or "Billing").
- An ESB in the middle performs routing, protocol translation, message transformation, and orchestration.
- Services often share a common canonical data model and sometimes shared databases.
- Governance is centralized: standards bodies and shared schemas.
The ESB concentrates logic and becomes both a bottleneck and a single point of failure, and changes to a shared model require coordination among many teams. Microservices are often described as a refinement of SOA that removes these two properties.
Microservices
A microservice architecture structures an application as a set of small, independently deployable services. Each service is built around a business capability, runs in its own process, and owns its data.
Defining properties:
- Independent deployability. A service can be built, tested, and released without releasing others.
- Data ownership. Each service is the only writer and reader of its datastore. Others access data through its API or events.
- Smart endpoints, dumb pipes. Logic lives in services. Communication uses simple protocols (HTTP/gRPC, or a message broker that only transports messages).
- Decentralized governance. Teams choose their own languages, datastores, and release schedule within organizational guardrails.
- Design for failure. Every remote call can fail or be slow, so callers apply timeouts, retries, and circuit breakers.
graph TD
subgraph M["Monolith: one deployment unit"]
direction TB
m_orders["Orders module"] --> m_db[("Single database")]
m_catalog["Catalog module"] --> m_db
m_pay["Payments module"] --> m_db
end
subgraph S["Microservices: one deployment unit per service"]
direction TB
s_orders["Orders service"] --> s_odb[("Orders DB")]
s_catalog["Catalog service"] --> s_cdb[("Catalog DB")]
s_pay["Payments service"] --> s_pdb[("Payments DB")]
s_orders -->|"network call"| s_catalog
s_orders -->|"network call"| s_pay
endComparison
| Aspect | Monolith | Modular monolith | SOA | Microservices |
|---|---|---|---|---|
| Deployment unit | One | One | Several, coarse | Many, fine |
| Module boundary enforced by | Convention only | Compiler/build/CI | Service contract | Process and network |
| Inter-module call | In-process | In-process via interface | Network, via ESB | Network, direct |
| Data | One shared database | One database, tables owned per module | Often shared | One datastore per service |
| Transactions across modules | ACID, local | ACID, local | Distributed or shared DB | Sagas, eventual consistency |
| Independent release | No | No | Partially | Yes |
| Independent scaling | No | No | Per service | Per service |
| Central component | None | None | ESB | None (gateway and broker only) |
| Operational complexity | Low | Low | High | High |
| Technology heterogeneity | No | No | Some | Yes |
What changes when a boundary becomes a network call
Moving a boundary from a function call to a network call changes the failure and consistency model:
- A call can time out with an unknown outcome: the callee may or may not have executed the request.
- Latency rises from nanoseconds to about a millisecond or more per hop within a data center, and it varies.
- Arguments must be serialized, which needs a versioned contract.
- A retried call may execute twice, so operations must be idempotent (safe to apply more than once).
- A workflow of five sequential calls at 2 ms each adds about 10 ms of network time that a function-call version does not have.
- A multi-step change across services can no longer use one ACID transaction.
- Testing, debugging, and deployment need distributed tooling: tracing, service discovery, and contract tests.
These costs are the price of independent deployment and scaling. They are examined in microservices trade-offs and the distributed monolith, and the criteria for accepting them are in when to use microservices.
Interview questions
- What is the difference between a modular monolith and microservices? Both have enforced module boundaries and data ownership per module. A modular monolith deploys as one unit and calls modules in-process. Microservices deploy each module independently and communicate over the network.
- How does SOA differ from microservices? SOA typically uses coarse services shared enterprise-wide, a central ESB for routing and orchestration, and often a shared data model or database. Microservices use fine-grained services owning their data, with logic in the services and simple communication channels.
- Is a monolith always a poor design? No. A monolith with enforced internal boundaries offers local transactions, simple deployment, and fast calls. Its limits are shared release cadence and shared scaling.
- Why can a well-built modular monolith be a step toward microservices? Its modules already depend on each other only through interfaces and own their tables, so a module can be moved behind a network API with limited change to its callers.
