Modular Monolith
A single deployable unit with rigorously enforced internal module boundaries — capturing the simplicity of a monolith while preserving the design discipline of microservices.
What it is
A modular monolith is a single deployable application whose internal structure is divided into well-defined modules with strictly enforced boundaries. Each module owns its own data schema (even if physically in one database), exposes a public API, and communicates with other modules only through that API — never by reaching directly into another module's internals or its database tables. Unlike a traditional layered monolith, the primary decomposition axis is vertical (by business capability) rather than horizontal (by technical tier). The result is a codebase that reads like a collection of services but deploys as a single unit, inheriting the operational simplicity of a monolith while retaining the design discipline that enables future service extraction.
Why it exists
The modular monolith emerged as a deliberate response to both the chaos of the "big ball of mud" monolith and the premature complexity of microservices. Teams repeatedly found that jumping directly to microservices before the domain was well understood led to wrong service boundaries that were expensive to change once they became network contracts. A modular monolith defers the expensive boundary-making decision while still forcing the discipline of thinking in terms of bounded contexts. Sam Newman and Martin Fowler both advocate modular monoliths as the preferred intermediate step: get the module boundaries right in a monolith first, then extract services only where operational pressure demands it.
When to use
- You want clean internal boundaries but are not ready (or do not need) to operate a distributed system.
- The domain model is actively evolving and you expect module boundaries to shift — in-process refactoring is far cheaper than changing service contracts.
- Team size is between 5 and 25 engineers and independent deployment is not yet a bottleneck.
- You want a migration path to microservices — a well-modularized codebase makes future service extraction surgical rather than traumatic.
- Organizational culture favors single-team ownership of the entire application.
When not to use
- Multiple independent teams need to deploy on different schedules without coordination — at this point the deployment monolith becomes the bottleneck, not the code structure.
- Components require different technology stacks or runtime environments.
- Scaling requirements are highly heterogeneous — the image processing pipeline and the login endpoint should not have to scale together.
Typical architecture
Modules communicate through explicit public interfaces. Each module may own logical schema namespaces in the shared database, enforced by convention and validated by architecture tests in CI.
┌────────────────────────────────────────────────────┐
│ Single Deployable Unit │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Orders │─────▶│ Inventory │ │
│ │ Module API │ │ Module API │ │
│ │ ─────────── │ │ ─────────── │ │
│ │ (internal) │ │ (internal) │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ internal events │ │
│ ┌───────▼──────────────────────▼───────┐ │
│ │ Internal Event Bus │ │
│ └──────────────────────────────────────┘ │
│ │
│ Architecture tests (ArchUnit) enforce no │
│ cross-module direct class access │
└────────────────────────────────────────────────────┘
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ orders.* │ │inventory.* │ (logical schemas)
└────────────┘ └────────────┘
Pros and cons
Pros
- Retains in-process call performance and ACID transactions.
- Enforced boundaries prevent the "big ball of mud" while staying in one deployment unit.
- Refactoring module boundaries is still cheap — no network contract changes.
- Single CI/CD pipeline and one deployable artifact to monitor.
- Easier developer onboarding — one repo, one run command, full debuggability.
Cons
- Still a deployment monolith — one failure can take down the entire application.
- Requires discipline and tooling to keep boundaries from eroding over time.
- Cannot independently scale a single module.
- Technology homogeneity — all modules share the same runtime and language.
Implementation notes
Enforce boundaries with automated architecture tests run in CI — ArchUnit for Java/Kotlin, Dependency Cruiser for JavaScript/TypeScript, or NDepend for .NET can assert that no class in orders.internal is imported by inventory.*. Organize each module around a public facade or service interface and mark internal packages explicitly. Use an internal event bus (synchronous or in-memory async) for cross-module communication to keep modules loosely coupled without network overhead. Each module should own its own database schema namespace (e.g., orders_* tables) and never let another module issue DML against those tables directly.
Common failure modes
- Boundary erosion under deadline pressure: Developers take shortcuts by directly accessing another module's repository or entity class, slowly rebuilding the big ball of mud.
- Missing architecture tests in CI: Without automated enforcement, boundary violations accumulate unnoticed until the codebase is again fully entangled.
- Shared transaction leakage: Modules join database transactions across module boundaries, creating hidden coupling that prevents future extraction.
- Circular dependencies: Module A depends on Module B which depends on Module A — prevents clean extraction and indicates a missing abstraction.
- Overly coarse modules: Modules that grow too large become mini-monoliths themselves; periodically audit and split as needed.
Decision checklist
- Have you identified the business capabilities that will map to modules (not technical layers)?
- Is each module's public API defined and documented before internal implementation?
- Do architecture tests run in CI to detect cross-module boundary violations?
- Does each module own its own schema namespace in the database?
- Is there a clear internal event bus strategy for cross-module async communication?
- Is there a documented extraction strategy for when a module needs to become a service?
Example use cases
- Shopify's "Component Rails": Shopify introduced modular boundaries into their monolith using Rails engines, enforcing module APIs before selectively extracting services.
- Mid-size SaaS platforms: A B2B SaaS with billing, user management, and core product modules that want clean boundaries but deploy on a shared Kubernetes pod.
- Greenfield products in regulated industries: Healthcare or finance startups that need audit trails and operational simplicity prefer a modular monolith before distributing the compliance surface area.
Related patterns
- Monolith Architecture — The simpler predecessor; a modular monolith adds disciplined boundaries.
- Microservices — The natural evolution when team autonomy and independent deployment become essential.
- Hexagonal Architecture — Often applied within each module to isolate domain logic from infrastructure.
Further reading
- Shopify's Modular Monolith — InfoQ talk on Shopify's journey.
- Modular Monolith Pattern — Sam Newman's canonical description.
- ArchUnit — Java architecture testing library for enforcing module boundaries.