Guide contents

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 public types, 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
    end

Comparison

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.