Loading content...
In architecture discussions, engineers often frame the choice as binary: monolith or microservices. This false dichotomy has led countless organizations down troubled paths—either struggling with an entangled monolith they can't evolve, or drowning in microservices complexity they weren't ready to handle.
But there's a compelling middle ground that combines the best of both worlds: the Modular Monolith. This architecture gives you the deployment simplicity of a single application while providing the organizational structure and future flexibility of a well-designed microservices system.
Pioneered by practitioners like Simon Brown and championed by companies like Shopify (which runs one of the world's largest modular monoliths), this pattern has proven itself at massive scale while avoiding the operational overhead of distributed systems.
By the end of this page, you will understand what a modular monolith is, why it represents a strategic sweet spot for many organizations, and how it positions you for future evolution—whether that's continued monolithic growth or strategic microservices extraction.
Before we dive into modular monoliths, let's establish the full spectrum of architectural choices. Understanding where each option sits—and what tradeoffs it embodies—is essential for making informed decisions.
The Traditional Monolith:
A traditional monolith is a single deployable unit where all code lives together, typically without clear internal structure. Components are tightly coupled, database tables are shared freely, and the boundary between different business domains is often blurred or nonexistent.
Characteristics:
The Big Ball of Mud:
The extreme form of the traditional monolith—code accumulated over years without structure, where dependencies form an incomprehensible web and changes require archaeology to understand impact. This is what monoliths become when not actively maintained.
| Architecture | Coupling | Deployment | Team Scalability | Operational Complexity |
|---|---|---|---|---|
| Big Ball of Mud | Extreme | Single unit | Very Poor | Low (but high change risk) |
| Traditional Monolith | High | Single unit | Limited | Low |
| Modular Monolith | Controlled | Single unit | Good | Low |
| Hybrid (Monolith + Services) | Medium | Few units | Good | Medium |
| Microservices | Loose | Many units | Excellent | High |
| Nano-services | Minimal | Very many units | Variable | Very High |
Key Insight: Coupling and Deployment Are Orthogonal
A critical realization is that internal structure (coupling) and deployment model are separate concerns. You can have:
The modular monolith chooses loose coupling with single deployment—gaining the benefits of structure without the costs of distribution.
Many organizations rush to microservices without addressing coupling first. The result is a 'distributed monolith'—services that must be deployed together, share databases, and require coordinated changes. You get all the complexity of distribution with none of the benefits. The modular monolith explicitly avoids this trap by handling coupling before distribution.
A modular monolith isn't just a monolith with folders. It's characterized by enforced boundaries, explicit contracts, and controlled coupling. The key word is enforced—the structure must be actively maintained, not just documented.
The Four Pillars of Modularity:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
my-modular-monolith/├── modules/│ ├── user/│ │ ├── internal/ # Private implementation│ │ │ ├── repository/│ │ │ ├── domain/│ │ │ └── services/│ │ ├── api/ # Public interface│ │ │ ├── UserService.ts│ │ │ ├── types.ts # Public DTOs│ │ │ └── events.ts # Domain events│ │ └── index.ts # Single export point│ ││ ├── order/│ │ ├── internal/│ │ │ ├── repository/│ │ │ ├── domain/│ │ │ └── services/│ │ ├── api/│ │ │ ├── OrderService.ts│ │ │ ├── types.ts│ │ │ └── events.ts│ │ └── index.ts│ ││ ├── inventory/│ │ ├── internal/│ │ ├── api/│ │ └── index.ts│ ││ └── payment/│ ├── internal/│ ├── api/│ └── index.ts│├── shared/ # Shared kernel (minimal)│ ├── types/│ ├── events/│ └── utils/│├── infrastructure/ # Cross-cutting concerns│ ├── database/│ ├── messaging/│ └── http/│└── main.ts # Application entry pointCritical: The Boundary Isn't Just a Folder
The folder structure above is necessary but not sufficient. What makes it a modular monolith is the enforcement layer:
12345678910111213141516171819202122232425
// Java with ArchUnit - Enforced at build time@ArchTeststatic final ArchRule modules_should_not_access_internals_of_other_modules = noClasses() .that().resideInAPackage("..order.internal..") .should().accessClassesThat() .resideInAPackage("..user.internal..") .orShould().accessClassesThat() .resideInAPackage("..inventory.internal..") .orShould().accessClassesThat() .resideInAPackage("..payment.internal.."); @ArchTeststatic final ArchRule modules_can_only_access_public_apis = classes() .that().resideInAPackage("..order..") .should().onlyAccessClassesThat() .resideInAnyPackage( "..order..", // Own module "..user.api..", // Other modules' public APIs only "..inventory.api..", "..shared..", // Shared kernel "java..", // Standard library "org.springframework.." // Framework );Without automated enforcement, module boundaries erode within months. Documentation is forgotten, 'quick fixes' bypass the rules, and the modular structure collapses into a traditional monolith. The investment in enforcement tooling is what preserves the architecture's value.
The modular monolith occupies a strategic position that maximizes benefits while minimizing costs. Understanding the specific advantages helps you articulate the case for this architecture.
Operational Simplicity:
A modular monolith is still a single deployment. This means:
Organizational Scalability:
Despite being a single deployment, teams can work independently:
The Optionality Value:
Perhaps the most underappreciated benefit is optionality. A modular monolith preserves flexibility:
You can stay as a monolith indefinitely — If the modular structure serves your needs and team size, there's no requirement to move to microservices. Many successful companies run modular monoliths at scale.
You can extract services surgically — Because modules have explicit boundaries and interfaces, extracting a module into a service is a well-defined, low-risk operation. You're not untangling a web—you're relocating a pre-packaged unit.
You can experiment — Run one module as a separate service while keeping others in the monolith. This hybrid approach lets you learn distributed systems incrementally.
You maintain velocity — While others are debugging network partitions, you're shipping features. The time saved on operational complexity can be redirected to product development.
Shopify's main application is one of the world's largest modular monoliths—millions of lines of Ruby serving enormous traffic. Rather than rushing to microservices, they invested heavily in modular structure, automated enforcement, and intentional boundaries. The result: team autonomy without distributed system overhead. Their experience demonstrates that modular monoliths scale far further than conventional wisdom suggests.
The modular monolith isn't universally correct—no architecture is. But there are situations where it represents the optimal choice, balancing current needs against future flexibility.
Optimal Scenarios:
When to Reconsider:
The modular monolith isn't always the answer. Consider alternatives when:
Instead of asking 'Should we use microservices?', ask 'What problem would microservices solve that a modular monolith can't?' If you can't articulate a specific, pressing problem, the modular monolith is likely the better choice.
Adopting a modular monolith requires a mental shift. You're not building microservices, but you're also not building a traditional monolith. The discipline falls somewhere in between.
Think Like You'll Extract, But Don't Optimize For It:
Design modules as if they could become services—clear boundaries, encapsulated data, explicit interfaces. But don't add complexity for extraction you may never do:
The goal is extraction-ready, not extraction-optimized.
Treat Module Boundaries as Team Contracts:
Even though you're in one codebase, treat module interfaces with the same care you'd treat service APIs:
The discipline required for a modular monolith is the same discipline required for successful microservices. Organizations that can't maintain module boundaries won't maintain service boundaries either. The modular monolith is a proving ground for architectural maturity.
Let's visualize the different architectural approaches to understand where the modular monolith fits:
Key Visual Insights:
Traditional Monolith: Everything connects to everything. Any change can impact any other part. The database is a single shared resource with no logical boundaries.
Modular Monolith: Clear boundaries exist between modules. Communication flows through public APIs. Internal implementation is hidden. The database may be shared but access is logically partitioned.
Microservices: Physical separation enforces boundaries. Network calls replace function calls. Each service has its own database. But this comes with significant operational overhead.
The modular monolith achieves the structural clarity of microservices without the network boundary. It's a logical separation rather than a physical one.
We've established the modular monolith as a strategic architectural choice—not a compromise, but an intentional optimization for specific tradeoffs. Let's consolidate the key insights:
What's Next:
Now that we understand what a modular monolith is and why it's valuable, the next page explores Module Boundaries—how to identify, define, and enforce the boundaries between modules. We'll examine Domain-Driven Design principles, dependency analysis techniques, and practical strategies for drawing lines that will serve your system for years to come.
You now understand the modular monolith as a strategic middle ground between traditional monoliths and microservices. It offers the simplicity of single deployment with the organizational benefits of clear boundaries. Next, we'll learn how to design those boundaries effectively.