The software industry is moving away from microservices. But the destination isn’t the traditional “big ball of mud” monoliths that teams fled in the first place. As part of our guide to modern software architecture, this article focuses on what makes modular monoliths different.
A modular monolith is a deliberate architectural choice. You structure your application with strong internal boundaries whilst keeping the operational simplicity of a single deployment.
We’re going to clarify the terminology, explain logical boundaries as the core differentiator, outline what you get from both architectural paradigms, and give you context for when this approach makes sense.
What Exactly Is a Modular Monolith?
A modular monolith is an architectural pattern where you build your application as a single deployment unit organised into independent, loosely coupled modules with well-defined boundaries. Unlike traditional monoliths, you enforce strong internal separation between modules. But you keep the operational simplicity of a unified deployment.
The pattern has two defining characteristics. First – single deployment unit. You compile it once, deploy it once, run it as one process. Second – strong logical boundaries between modules. These aren’t suggestions. They’re architectural rules backed by tooling and testing.
Modules are logical groupings within one codebase and one process. Think of them as internal services that share the same memory space. This contrasts with “big ball of mud” traditional monoliths where everything touches everything, and physically separated microservices where modules run in separate processes with network boundaries between them.
Kamil Grzybek explains it this way: “Modular Monolith architecture is an explicit name for a Monolith system designed in a modular way.” Monolith architecture doesn’t imply poor design. It describes deployment topology.
Each module has everything it needs to provide its functionality and exposes a well-defined interface. The approach significantly improves system cohesion whilst keeping things operationally simple.
This is a deliberate architectural choice, not a legacy constraint you’re stuck with.
How Does a Modular Monolith Differ from a Traditional Monolith?
Traditional monoliths lack clear architectural boundaries. Components become tightly coupled and interwoven. Modular monoliths enforce strong logical boundaries between modules. Each module has well-defined public interfaces and encapsulated implementations. This prevents the entanglement that characterises traditional monolithic codebases.
“Big ball of mud” describes the anti-pattern – tight coupling, no clear boundaries, high interdependence. Traditional monoliths can be well-designed, but modular monoliths enforce discipline.
In traditional monoliths, code changes trigger unintended side effects in unrelated systems. The codebase becomes a web of dependencies. Touch one component and risk breaking three others.
Modular monoliths prevent this through enforced encapsulation. Modules expose public interfaces and hide implementation details. Changes within a module stay within that module, as long as the public interface remains stable.
Here are the key differences:
Boundaries: Traditional monoliths have fuzzy or non-existent boundaries. Modular monoliths have explicit, enforced boundaries.
Coupling: Traditional monoliths trend toward tight coupling. Modular monoliths maintain loose coupling through interface contracts.
Module Independence: Traditional monoliths offer little independence. Modular monoliths enable independent module development and testing.
Refactoring: Traditional monoliths make refactoring risky. Modular monoliths make it safer.
Team Organisation: Traditional monoliths typically have one large team. Modular monoliths support module-based teams.
Modular monoliths can remain maintainable even at very large scale.
You can refactor a traditional monolith into a modular monolith by identifying module boundaries and enforcing encapsulation. The specific techniques for enforcing these boundaries in practice are covered in our implementation guide.
What Are Logical Boundaries and Why Do They Matter?
Logical boundaries are architectural separation lines within a codebase that define module ownership, public interfaces, and private implementation details. You don’t need physical deployment separation. They enable module independence, let teams work autonomously, prevent coupling escalation, and preserve the option to extract modules as microservices later.
Derek Comartin emphasises that logical boundaries group functionality or capabilities, not just entities. Logical boundaries exist in code organisation, not infrastructure.
Here’s what makes up logical boundaries:
Namespace or package structure: Modules are organised into distinct namespaces that signal ownership and prevent accidental coupling.
Access control: Only the public interfaces of a module are accessible to other modules. Internal implementation details stay hidden.
Defined interfaces: Each module exposes a clear API contract that other modules use for communication.
Data ownership: Each module owns its data and exposes operations through its public interface rather than allowing direct database access.
The benefits are substantial. Module independence means you can modify a module’s internals without affecting others. Team autonomy means different teams can own different modules with minimal coordination. Reduced cognitive load means developers can focus on one module without understanding the entire system.
This enables “thinking in modules” despite single deployment.
In microservices, boundaries are enforced by network separation. In modular monoliths, boundaries are logical and require more discipline. There’s no compiler enforcement. You have to respect module interfaces voluntarily, backed by architectural testing and code review.
Domain-Driven Design provides the methodology for identifying these boundaries through bounded contexts. Each bounded context owns its own ubiquitous language.
Consider a vehicle in a logistics system. In recruitment, the vehicle concept is about compliance – insurance, registration. In dispatch, it’s about executing a shipment – capacity, location. Same entity, different bounded contexts, different logical boundaries.
Understanding broader architectural approaches helps contextualise why logical boundaries matter. They’re the key mechanism that separates modular monoliths from traditional counterparts.
Modular Monolith vs Loosely Coupled Monolith vs Majestic Monolith – Are They the Same?
These terms are largely the same thing. They all refer to monolithic architectures with strong internal modularity. “Modular monolith” is most common. “Loosely coupled monolith” emphasises messaging. “Majestic monolith” emphasises the positive qualities. All describe single-deployment architectures with disciplined module boundaries.
“Modular Monolith” is the standard industry term. Most technical writing uses this term as the default.
“Loosely Coupled Monolith” is Derek Comartin’s preferred term. The emphasis is on asynchronous communication patterns between modules, often using in-process message buses.
“Majestic Monolith” is Basecamp‘s branding. It emphasises pride in well-architected monolithic systems.
“Domain-Oriented Monolith” emphasises DDD-based module organisation. Modules align with DDD subdomains and bounded contexts rather than technical layers.
All variants describe the same core pattern – single deployment with module discipline.
These terminology differences reflect emphasis rather than fundamental architectural differences. Whether you call it modular, loosely coupled, or majestic, you’re describing an architecture that combines deployment simplicity with modularity discipline.
What Benefits Do Modular Monoliths Inherit from Microservices?
Modular monoliths adopt microservices’ architectural discipline – strong module boundaries, clear interfaces, domain-driven design, team ownership of modules, and independent module development. The key inheritance is modularity thinking, not distributed deployment.
Module independence through well-defined interfaces works similarly to service APIs. Each module exposes operations through a public interface.
Team autonomy via module ownership uses code boundaries instead of deployment boundaries. Different teams own different modules and can work independently with minimal coordination.
Domain-driven design provides the methodology for identifying module boundaries, just as it identifies service boundaries in microservices.
Clear contracts between modules mirror inter-service contracts. Modules communicate through public APIs with defined inputs, outputs, and behaviour.
Fast flow through reduced coordination becomes possible. Teams work independently on modules without constant synchronisation.
Having a modular architecture allows extracting modules into separate services when needed. The strangler fig pattern becomes viable because modules already have defined boundaries.
You’re thinking in terms of service-like modules whilst keeping everything in one deployment unit. When to extract modules to microservices becomes a decision you can make later.
What Operational Simplicity Do Modular Monoliths Preserve from Traditional Monoliths?
Modular monoliths maintain monolithic operational simplicity – single deployment pipeline, in-process communication, simplified debugging, no distributed tracing requirements, straightforward transaction management, and reduced infrastructure complexity.
Single deployment means one pipeline, one deployment, simplified release coordination. You’re not orchestrating deployment sequences across dozens of services.
In-process communication means nanosecond method calls instead of millisecond network calls. Communication between modules occurs in-process with no network latency or serialisation overhead.
If you’re making thousands of calls during request processing, the performance difference adds up fast. Each method call stays in memory rather than crossing network boundaries.
Logging and tracing becomes easier by not distributing the system. Stack traces work across modules. Logs come from one application.
Calls are less likely to fail since they’re inside the same machine. You don’t need circuit breakers, retries, or fallback strategies.
Transaction management stays straightforward. Modules participate in the same transactions. Standard ACID transactions work across modules. No saga patterns, no eventual consistency, no distributed transaction coordinators. Just regular transactions that either commit or rollback.
Infrastructure simplicity means no service mesh, no container orchestration complexity. No orchestration tools, service discovery, or complex deployment strategies.
You don’t need Kubernetes experts or distributed systems specialists. Troubleshooting, logging, and monitoring are straightforward.
The detailed cost breakdown of microservices operational complexity illustrates what you’re avoiding.
How Do Modular Monoliths Achieve the Best of Both Worlds?
Modular monoliths combine monolithic operational simplicity with microservices architectural discipline. This lets teams stay productive and operationally manageable whilst building systems that remain maintainable at scale.
From monoliths, you get single deployment, in-process calls, simple debugging, transaction management, and reduced infrastructure. From microservices, you get module boundaries, team autonomy, domain-driven design, clear interfaces, and refactoring safety.
You avoid tight coupling (the traditional monolith problem) and distributed complexity (the microservices problem). It’s about taking what works from each approach and leaving behind what doesn’t.
The value isn’t in the technology itself. It’s in what the architecture enables – teams that can move independently without stepping on each other’s toes, code that can scale without becoming unmaintainable, and operational simplicity that doesn’t require a team of specialists to keep the lights on.
The pattern delivers single deployed applications with well-separated modules, clear boundaries, and in-memory communication.
There’s a trade-off. Modular monoliths require more discipline than traditional monoliths because boundaries aren’t physically enforced. You can’t rely on the network to prevent one module from directly accessing another module’s internals. You need code reviews, architectural testing, and team discipline. But this is still significantly less overhead than managing distributed systems with all their failure modes, deployment complexity, and monitoring requirements.
The architectural evolution path isn’t linear. Each architecture suits different contexts. The trend of consolidating from microservices back to modular monoliths demonstrates this.
Real-world examples of companies using this approach successfully span different scales and domains.
When Does a Modular Monolith Make More Sense Than Microservices?
Modular monoliths make sense for teams under 50 developers, organisations with limited operational capacity for distributed systems, domains with medium complexity, and contexts where deployment coupling is acceptable.
The team size threshold provides guidance. 1-10 developers should build monoliths. 10-50 developers fit modular monoliths perfectly. Only at 50+ developers with clear organisational boundaries and proven scaling bottlenecks do microservices justify their cost.
Most teams never reach the 50+ developer threshold yet rush into distributed architectures anyway.
Operational capacity matters more than team size for some organisations. If you have small SRE expertise and can’t support service mesh and Kubernetes complexity, microservices are impractical. If your entire technical team is three developers and none of them has Kubernetes experience, you shouldn’t be running microservices. Full stop.
Domain complexity influences the decision. Smaller projects with well-defined boundaries may thrive within a modular monolith. If your domain is complex enough to benefit from modularity but not complex enough to require physical separation, modular monoliths hit the sweet spot.
Transaction requirements provide another decision factor. When most operations stay within module boundaries and need ACID across modules, modular monoliths handle this elegantly.
Performance sensitivity to latency matters. Latency-critical operations benefit from in-process calls.
Business stage affects the calculus. Modular monoliths are a great fit for launching startups, keeping development simple and reducing operational overhead.
Kirsten Westeinde from Shopify recommends that new products and new companies start with a monolith. Martin Fowler agrees – you shouldn’t start a new project with microservices, even if you’re sure your application will be big enough to make it worthwhile.
What modular monoliths aren’t good for – extreme scale requiring independent service scaling, polyglot requirements, globally distributed teams needing independent deployment.
This is a preview of the decision framework. We expand it fully in the comprehensive decision framework. The validation comes from industry trends. 42% of organisations are consolidating from microservices, confirming that modular monoliths make sense for more contexts than initially assumed.
FAQ Section
What is the main difference between a monolith and a modular monolith?
The main difference is architectural discipline. Traditional monoliths lack enforced boundaries between components, allowing tight coupling. Modular monoliths enforce strong logical boundaries with well-defined module interfaces, enabling module independence whilst maintaining single deployment.
This conceptual understanding provides the foundation for implementation patterns covered in the implementation guide.
Can a modular monolith scale to millions of users?
Yes. Shopify runs a 2.8 million line-of-code modular monolith containing 500,000 commits. They support hundreds of thousands of merchants. Scaling comes from caching strategies, read replicas, database optimisation, and selective horizontal scaling.
The detailed case study covers Shopify’s scaling approach.
Is a modular monolith just poor microservices implementation?
No. A modular monolith is a deliberate architectural choice that optimises for operational simplicity whilst maintaining modularity. It’s a distinct pattern suited to different contexts like smaller teams, limited operational capacity, and lower complexity – not a compromise or inferior implementation of microservices.
The decision framework provides guidance for choosing between patterns.
How do you prevent a modular monolith from becoming a “big ball of mud”?
Prevention requires architectural discipline through enforced module boundaries, module APIs as access points, code ownership by teams, and architectural testing. Unlike traditional monoliths, modular architectures use tools like ArchUnit and NDepend to automatically detect boundary violations.
Domain-driven design, hexagonal architecture, and regular refactoring maintain these boundaries over time.
Boundary enforcement mechanisms and tooling get covered in the implementation guide.
What is hexagonal architecture and how does it relate to modular monoliths?
Hexagonal architecture (ports and adapters) is an implementation pattern for modular monoliths. It defines clear boundaries via ports (interfaces) and adapters (implementations), enabling module independence and testability.
Implementation guidance for hexagonal architecture in modular monoliths comes in the implementation guide.
Can you migrate from a traditional monolith to a modular monolith?
Yes. Migration involves identifying module boundaries (often via Domain-Driven Design), creating module interfaces, enforcing encapsulation, and gradually refactoring to reduce coupling.
Start by following Domain-Driven Design to keep business logic modular. Organise related functionalities into well-defined domains ensuring each module remains independent.
Migration processes get detailed coverage in the migration guide.
What’s the relationship between Domain-Driven Design and modular monoliths?
Domain-Driven Design provides methodology for identifying module boundaries via bounded contexts. Many modular monoliths use DDD bounded contexts to define module structure, ensuring modules align with business domains rather than technical layers.
Implementation patterns appear in the implementation guide.
Do modular monoliths support microservices-style messaging patterns?
Yes. Modular monoliths can use in-process message buses or event-driven patterns for asynchronous module communication. This enables temporal decoupling whilst remaining in a single deployment unit.
Internal messaging infrastructure setup gets covered in the implementation guide.
How many modules should a modular monolith have?
Module count depends on domain complexity and team structure. Common range is 5-20 modules. Shopify established 37 components in their main monolith, each with public entry points.
If two modules are very “chatty”, you might have incorrectly defined the boundaries and should consider merging them. Align module boundaries with team boundaries and domain bounded contexts.
Module sizing strategies get discussed in the implementation guide.
Is a modular monolith the same as a service-oriented architecture (SOA)?
No, though they share modularity principles. SOA typically involves network-separated services communicating via protocols like SOAP or REST. Modular monoliths use in-process communication within single deployment. SOA is closer to microservices in deployment model.
What makes a monolith “majestic” vs just “modular”?
“Majestic monolith” is Basecamp’s branding emphasising pride in well-architected monolithic systems. It’s marketing terminology rather than technical distinction – it refers to the same modular monolith pattern with strong boundaries.
Basecamp’s advocacy gets detailed coverage in the case studies.
Can parts of a modular monolith scale independently?
Yes, with limitations. You can use read replicas for read-heavy modules, caching strategies for specific modules, database partitioning by module, and extract high-load modules to separate services.
During holiday season, bookings and payments modules can be deployed independently, then merged back into single deployment afterward.
Selective scaling strategies get covered in the implementation guide.