A practical lesson on modular monoliths as a strong way to enforce business boundaries, ownership, and extraction readiness without taking on distributed-systems cost too early.
A modular monolith is one of the most useful architecture styles for teams that need stronger boundaries but do not yet need a fleet of distributed services. It keeps one deployable application while making internal seams explicit. That matters because many service extractions fail not because the idea of decomposition was wrong, but because the team distributed the system before it had proved the boundary internally.
The modular monolith solves a specific problem: how to treat boundaries as real architecture without forcing every boundary to become a separate runtime, deployment, database, and incident surface. It is boundary-first rather than distribution-first.
flowchart TD
A["Modular monolith"] --> B["One deployable unit"]
A --> C["Explicit module APIs"]
A --> D["Dependency rules"]
A --> E["Clear ownership"]
A --> F["Possible later extraction"]
A modular monolith is not just a project with folders named after domains. The difference is enforcement. Teams should know:
Without those rules, “modular monolith” often becomes a polite label for a conventional monolith with nicer package names.
The modular monolith is often the strongest first move because it allows teams to:
This makes it especially valuable for product teams that are growing but not yet ready to operate many services independently.
Module boundaries become real through constraints. The code example below uses dependency-cruiser to block imports into another module’s internal code.
1module.exports = {
2 forbidden: [
3 {
4 name: "no-cross-module-internals",
5 from: { path: "^src/modules/([^/]+)/" },
6 to: {
7 path: "^src/modules/([^/]+)/internal/",
8 pathNot: "^src/modules/$1/",
9 },
10 },
11 {
12 name: "no-direct-db-crossing",
13 from: { path: "^src/modules/([^/]+)/" },
14 to: { path: "^src/modules/([^/]+)/persistence/", pathNot: "^src/modules/$1/" },
15 },
16 ],
17};
What this demonstrates:
The common mistake is to define modules but allow unrestricted imports “for speed.” That usually recreates the same coupling that later makes extraction painful.
A strong modular monolith gives teams a way to ask, “If this module later becomes a service, what would change?” That is a healthier question than “What services can we create right now?” A module is often a good extraction candidate when:
If those conditions are absent, extraction will often move coupling rather than reduce it.
A team says it already has a modular monolith because its code is organized into catalog, billing, and checkout folders. However, any module may import any other module’s internals, and several workflows still read every table directly. Is this actually a modular monolith?
Not in the stronger architectural sense. The folders may hint at the right shape, but without enforced dependency rules and meaningful ownership boundaries the structure is mostly cosmetic.