The Modular Monolith
This page explains the core architectural philosophy behind this template.
What is a Modular Monolith?
A modular monolith is an architectural approach that combines the deployment simplicity of a monolith with the organizational benefits of well-defined modules. ThoughtWorks describes it as:
“A set of modules with specific functionality, which can be independently developed and tested, while the entire application is deployed as a single unit.”
Definition and Key Characteristics
A modular monolith is:
A single deployable unit that can be built, tested, and deployed as one artifact
Organized into domain modules with clear boundaries and responsibilities
Built with explicit interfaces between modules rather than implicit dependencies
Designed so modules can be independently developed and tested even though they deploy together
Single Deployable Unit, Multiple Domain Modules
You ship one application, but internally it’s organized by business domain:
Users and authentication
Billing and subscriptions
Core product features
Notifications and messaging
Each domain is a module with its own models, services, and APIs.
Why Not Microservices (Yet)?
Microservices are powerful for large organizations with mature platform teams. For most teams starting out, they introduce complexity that outweighs their benefits.
Distributed Computing Complexity
Distribution should be avoided unless the benefits clearly outweigh the costs. If you can keep it all in one app, you have a much better chance of keeping it all in your head too.
Microservices introduce distributed computing challenges:
Network failures between services
Data consistency across service boundaries
Complex deployment orchestration
Service discovery and load balancing
These aren’t insurmountable, but they require investment in tooling and expertise.
Operational Overhead
Running microservices means:
Multiple deployment pipelines
Distributed logging and tracing
Cross-service debugging
Version compatibility management
A small team managing a dozen services often spends more time on operations than features.
When Microservices Make Sense
Microservices become valuable when:
Different parts of your system need to scale independently
Teams are large enough to own separate services
You need different technology stacks for different problems
Your domain boundaries are well understood and stable
The modular monolith lets you discover these boundaries before committing to distribution.
Why Not a Traditional Monolith?
A traditional monolith without internal structure creates its own problems.
Spaghetti Dependencies
Without clear boundaries, code depends on code arbitrarily. A change in the billing module breaks authentication because someone took a shortcut. The dependency graph becomes a tangled mess—what some call ending up with a “Distributed Monolith” even with code in a single repository.
Difficulty Onboarding New Developers
New team members can’t understand a slice of the system—they have to understand everything to change anything. Onboarding takes weeks, and developers are afraid to make changes.
Scaling Team Becomes Scaling Problems
Adding developers doesn’t increase velocity because everyone is stepping on each other. Merge conflicts are constant. Coordination overhead dominates.
The Best of Both Worlds
The modular monolith gives you the best of both approaches.
Modularity Without Distribution
You get clear boundaries and separation of concerns without the operational complexity of distributed systems. Modules communicate through explicit interfaces, but those calls are in-process, not over the network.
In practice, this means using an in-memory event bus: modules publish domain events when significant things happen, and other modules subscribe to react. The event bus is a simple pub-sub mechanism that routes events to registered handlers, all within the same process.
This is significantly easier to design, deploy, and manage because modules ship together with optimized inter-module communication.
The same event contracts that work in-memory can later be routed through RabbitMQ, AWS SNS, or other message brokers when you need independent scaling or service extraction. Your handlers stay the same. Only the transport changes.
Strong Boundaries Enable Future Extraction
When you do need to extract a service (because it needs independent scaling, or a separate team will own it), you have a clean seam. The module already has a defined interface. Extraction is straightforward, not a multi-month rewrite.
The goal is to identify good architectural boundaries before extracting code into independent services. This sets you up to migrate to microservices in the future if needed.
Grow Your Architecture With Your Team
Start simple. Add modules as your domain expands. Extract services when the organizational or technical need is clear. Your architecture evolves with your business rather than constraining it.
The Modular Monolith is simple in its concepts, but powerful in enabling teams to scale their software.
Supporting Research
This approach draws from several practitioners who’ve written about their experiences:
DHH (Basecamp): “If you can keep it all in one app, you have a much better chance of keeping it all in your head too.”
Dan Manges (Root Insurance): “We’ve heard of teams ending up with a Distributed Monolith: code in independent services that is as difficult to work with as a Monolith. One underlying cause of that is poor architecture.”
ThoughtWorks: The modular monolith is “significantly easier to design, deploy and manage” because modules ship together with optimized inter-module communication.
Further Reading
The Majestic Monolith — DHH on why small teams should embrace monoliths
The Modular Monolith: Rails Architecture — Dan Manges on structuring code by domain at Root Insurance
Modular Monolith: A Better Way to Build Software — ThoughtWorks on the modular monolith as a middle ground