Modular Monoliths & Internal Architecture
A modular monolith is a single deployable system with explicit internal boundaries. It is not a polite name for a tangled codebase. It applies information hiding and bounded-context thinking inside one runtime so deployment, transactions, debugging, and local development stay simple while domain ownership remains visible. For many products, especially those built by small or medium teams, it is the most underused senior-level architecture.
The core idea is simple: pay for distribution only when you need distribution. A monolith can contain independent domains, ports, adapters, internal events, private persistence schemas, and strict dependency rules. It can also produce excellent operational behavior because there is one runtime, one deployable, one debugger path, and fewer network failure modes. The challenge is discipline. Without enforcement, a modular monolith tends to dissolve into shared utilities, direct table access, and cross-module shortcuts.
Internal Boundaries
Internal boundaries need more than folders. They need dependency rules, public APIs, private implementation, test coverage, and review habits. A module should expose capabilities and hide its data structures. Other modules should call its public interface or consume its published events, not reach into its repositories or tables.
This is where language matters. A “service” inside a monolith can become a bag of methods unless it is attached to domain responsibility. “OrderPlacement” is clearer than “OrderService” if the capability is placing an order. “BillingAccount” is clearer than “CustomerEntity” if the context owns legal invoicing behavior. Internal architecture should help developers speak in the domain language, not merely organize technical layers.
Dependency Rules
Dependency rules are the immune system of a modular monolith. Common rules include: domain modules may not depend on web frameworks, modules may not import each other’s private packages, shared code must be stable and small, infrastructure depends inward, and data access must go through owning modules. These rules should be automated with architecture tests or static analysis. Social discipline alone decays under deadline pressure.
The shared kernel deserves special suspicion. It should contain concepts that are genuinely stable across contexts: identifiers, money primitives, time abstractions, result types, or security principal shapes. It should not become the place where disputed domain concepts go to avoid ownership conversations. When everything is shared, nothing is owned.
Data Inside the Monolith
A modular monolith may use one physical database, but it should not treat all data as communal. Separate schemas, table naming, repository ownership, or database permissions can reinforce boundaries. The important rule is that one module owns the invariants for its data. Other modules can request actions, subscribe to events, or read published views.
Transactions are one of the advantages of a monolith, but they should be used deliberately. A single transaction across modules may be appropriate for a genuine invariant. It may also be a sign that the boundary is wrong or that the business process needs a saga-style workflow. The fact that a transaction is easy does not mean it is architecturally harmless.
Extractability
One promise of a modular monolith is that a module can later be extracted into a service. That promise is only credible if the module already owns its data, has a narrow public contract, avoids private imports from other modules, and can be tested independently. Extraction is not mainly about moving code into another repository. It is about replacing in-process calls with network calls, replacing local transactions with distributed workflows, and creating independent deployment and operations.
Designing for possible extraction does not mean prematurely building microservices. It means keeping seams honest. Use module APIs that could become remote without changing business semantics. Publish events that could move to a broker later. Keep data ownership clear. Track dependencies. These practices improve the monolith even if extraction never happens.
Practice
Take an existing monolith or imagine one. Define five modules, the owner of each module, its public interface, its private data, and one forbidden dependency. Then choose one module that might be extracted in the future and list what would have to become explicit before extraction is safe.
References & Further Reading
- David L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules” (Communications of the ACM, standard copyright)
- Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (Addison-Wesley, standard copyright)
- Martin Fowler and James Lewis: Microservices (standard copyright)
- Monolith to Microservices by Sam Newman (O’Reilly, standard copyright)