Modular Monolith: The Right Default for Most Systems
What This Concept Is
A modular monolith is a single-deployment system whose internal structure is organized into explicit, well-bounded modules. It is the pragmatic midpoint between "one big layered application" and "microservices":
- Single binary, single deploy. Everything runs in one process.
- Multiple logical modules. Code is grouped by domain capability (
catalog,checkout,fulfillment), not by technical layer. - Enforced module boundaries. Modules expose public APIs to each other; everything else is internal. The rules are enforced by tools (Concept 6), not just convention.
- Usually one database, but with per-module schemas or tables. Each module owns its data.
+---------------------------------------------------------------+
| One deployable binary |
| |
| +----------+ +------------+ +-------------+ |
| | catalog |<----| checkout |---->| fulfillment | |
| | (api) | | (api) | | (api) | |
| | -------- | | ---------- | | ----------- | |
| | domain | | domain | | domain | |
| | db: cat_*| | db: chk_* | | db: ful_* | |
| +----------+ +------------+ +-------------+ |
| |
| [ Shared platform layer ] |
| (auth, logging, metrics, HTTP, DB pool) |
+---------------------------------------------------------------+
Cross-module calls are in-process function calls across module API surfaces, not HTTP. That is the entire trick: you keep the operational simplicity of a monolith and the conceptual structure of a services system.
Why It Matters Here
Modular monolith is the right default for most systems in most teams. It is the answer to the "microservices or monolith?" question that refuses to be either. Its value proposition:
- Cheap to run. One binary, one deploy pipeline, one set of observability. No network hops inside the system.
- Cheap to refactor. Moving a function from
catalogtocheckoutis an IDE refactor, not a multi-team coordination event. - Easy to reason about. You can run the whole system locally.
- An explicit boundary catalog. Because modules have APIs, you already have the lines along which the system could be split later if a quality-attribute requirement changes.
It is what Fowler refers to when he says "monolith first": start here, split only when pain becomes concrete. And it is what the Shopify engineering team has been loudly defending for their Rails codebase -- a very modular monolith enforced with Packwerk.
Concrete Example
Take an OrderFlow-style commerce platform. A modular monolith layout:
src/
catalog/
api/ <- public: Product, Price, Inventory lookups
domain/ <- internal: aggregates, services
persistence/ <- internal: repos, DB schema catalog_*
checkout/
api/ <- public: StartCheckout, Commit
domain/
persistence/ <- DB schema checkout_*
fulfillment/
api/ <- public: AllocateStock, Ship
domain/
persistence/ <- DB schema fulfillment_*
platform/
auth/ <- shared
http/
metrics/
Checkout calls Catalog through catalog.api.ProductLookup, not by importing catalog.domain.product.Product directly. Checkout cannot touch catalog_* tables. Catalog never calls out of src/catalog/**.
When the checkout team ships a change to pricing rules, they change checkout/domain/. They do not coordinate with the catalog team. They deploy one binary; the catalog API they depend on is unchanged, so the catalog team does not need to be in the room.
What would happen if we went layered instead:
src/controllers/,src/services/,src/repositories/-- a catalog change and a checkout change touch the same folders, collide in code review, and contaminate each other's tests.- there is no single place to see "the checkout subsystem"
- splitting later requires a rewrite, because layers do not factor along domain lines
What would happen if we went microservices instead:
- three services, three CI pipelines, three databases, three auth integrations
- a price-lookup that was a 0.5ms function call is now an HTTP round trip with retries, timeouts, and circuit breakers
- the team spends the first six months building platform, not product
Common Confusion / Misconception
"Modular monolith is just a monolith with folders." Only if the boundaries are enforced. A folder named checkout/ whose files routinely import catalog.domain.Product is a monolith with folders. A folder named checkout/ whose imports are checked by Packwerk or ArchUnit (Concept 6) is a modular monolith. The difference is the enforcement, not the naming.
"It is just microservices without HTTP." No. In-process modules share the process, the memory space, the transaction boundary, the deploy cycle. That is a feature, not a deficiency. A shared transaction across catalog and checkout is a single DB transaction; across two microservices it requires a saga. Avoiding the saga when you do not need it is the whole point.
"The database is still shared, so it is not really modular." Shared server, not shared schema. Each module's tables are private; other modules talk through the module API. This is exactly the service-based architecture pattern (Concept 7) with the boundary at the process API instead of an HTTP service. If someone reaches into another module's tables, that is a violation, and Packwerk-style rules can catch it.
"This is the same thing as hexagonal architecture." Hexagonal is about isolating the domain from frameworks (ports and adapters). Modular monolith is about isolating modules from each other at a larger grain. They compose: each module can be internally hexagonal.
How To Use It
Operational checklist before calling a system a modular monolith:
- Every module has an
api/directory (or equivalent public surface). No one imports fromdomain/orpersistence/of another module. - Each module owns its persistence (a schema, a prefix, or distinct tables). Cross-module joins are replaced by API calls or materialized read models.
- A boundary-check fitness function (Concept 6) is in CI and fails the build on violations.
- Each module has its own set of tests that can run without the rest.
- The deploy story is one binary. Do not tolerate a "modular monolith" that already needs two services to start.
Check Yourself
- Why is modular monolith described as the "right default" by Fowler and Newman? Name the specific alternative it dominates and why.
- What is the difference between a monolith, a modular monolith, and a service-based architecture (Concept 7)? List the boundary drawn in each.
- What is the one change of circumstance that should push a team from modular monolith to extracting a service? (Hint: it is not "we have more engineers.")
Mini Drill or Application
Take a codebase you know (work, open source, or a class project). In 20 minutes:
- List the domain capabilities (5-10 bullets).
- Draw a mermaid graph of modules, one per capability.
- For each cross-module edge, label whether it is currently an in-process call, a DB join, a shared class, or broken. Broken edges are the first target for Concept 5 and 6.
- Name one module whose boundary is solid today (honest signal) and one whose boundary is fiction.