The modular monolith architecture isn’t new, but it’s become more popular as a way to balance the simplicity of a monolithic application with the flexibility of microservices. It promises to be the best of both worlds—until it isn’t. If you’re not careful, your modular monolith can quickly become just as tangled and unmanageable as a traditional monolith. Here are five common pitfalls that can lead you down that path and how you can steer clear of them.
1. Forgetting to Enforce Boundaries
It is easy to violate module boundaries, if there is no tooling in place to enforce it. Module boundaries must be preserved at all logical and physical layers of an application. Module “Book” cannot make an SQL JOIN in the persistance layer and join table customer. This is violation of module boundaries on a database level. The same rule must be applied to all “module” layers. For “Book” module cannot reference “Order” service directly. As a result you will end up with tightly coupled modules, which loses the benefits of this architectural style.
How to Avoid This:
- Communicate through an module API, instead of directly referencing services and data structures from different modules.
- If you have all code in one repository tools like “ts-arch” for TypeScript project may help to define rules, which can be enforced on the CI.
2. Incorrect Module Boundaries
A modular monolith depends on clear boundaries between modules. These boundaries ensure that each module is self-contained, with its own logic and data. But what happens if you let those boundaries blur? You get tightly coupled modules that depend on each other’s internals, making your application fragile and hard to maintain.
How to Avoid This:
- Clear Interfaces: Define and stick to clear interfaces for each module. This is non-negotiable.
- Boundary Enforcement: Use tools and rigorous code reviews to ensure that modules don’t cross into each other’s territory.
3. Overengineering Modules
In an effort to make each module independent, you might be tempted to overengineer. Each module ends up being its own mini-application, complete with separate infrastructure, communication patterns, and even data storage. Sounds robust? It’s actually a recipe for complexity that can slow down development and integration.
How to Avoid This:
- Balance Independence: Your modules should be independent enough to be developed in isolation, but not so independent that they duplicate effort or infrastructure.
- Avoid Premature Optimization: Don’t overcomplicate things by trying to predict every possible future need. Build for what you need now, and adapt as necessary.
- Lack of communication: Development in application modules creates knowledge silos, because teams have different goals/tasks and have no incentive to align decisions.
3. Lack of Cross-Team Communication
When different teams work on different modules without proper communication, you end up with a fractured system. Inconsistent implementations and duplicated logic are just the beginning. The lack of alignment makes integration difficult and increases maintenance costs.
How to Avoid This:
- Encourage Collaboration: Regular communication between teams is essential to keep everyone aligned.
- Shared Documentation: Maintain shared documentation on common practices, data models, and coding standards.
- Cross-Module Reviews: Implement cross-module code reviews to ensure consistency across the system.
- Community of Practice: Community of Practice tackles the knowledge silo problem. Where each team (maintaining n+1 modules) has its own practicies. To have common practicies you need to communicate and apply those practicies. This is why CoP is vital in the scope of a Modula Monolith. This is an organizational problem, which is hard to be solved with an techical means.
4. Ignoring Domain-Driven Design (DDD)
Domain-Driven Design (DDD) is key to aligning your modules with your business capabilities. But if you ignore DDD principles, your modules may not map to actual business needs, making them harder to understand and evolve over time. If applicable module boundaries are implemented at the start of architecture, then it will be very hard to evolve application. You will end up with either to grannular modules, which will not be self sufficent, or with too fat modules, which will loose benefits of modular monolith.
How to Avoid This:
- Embrace DDD: Use DDD to guide the design of your modules, ensuring they align with business domains.
- Focus on Business Capabilities: Each module should represent a specific business capability. If it doesn’t, reconsider your design.
- Bounded Contexts: Clearly define bounded contexts so that each module has a well-defined responsibility and doesn’t overlap with others.
5. No Comprehensive Testing Strategy
Testing is crucial, even in a modular monolith. Without a solid testing strategy, you might find that your modules don’t play well together, leading to unexpected bugs in production. Relying only on unit tests or skipping integration tests? That’s asking for trouble.
How to Avoid This:
- Layered Testing: Use a mix of unit, integration, and end-to-end tests to cover all bases in the modules.
- Test Module APIs: You need to test module API and better to develop htis API practicing TDD. Then you will not have APIs that are not used or bloated APIs.
- CDC: Consumer driven Contracts, are tests defined by a consumer. This allows Provider of a public API to know what API is used and how it is used. It allows to avoid breaking changes.
Conclusion
Building a modular monolith can offer a great balance between simplicity and flexibility, but it’s easy to fall into traps that can make it as unmanageable as a traditional monolith. To avoid this, enforce clear module boundaries, define modules correctly, avoid overengineering, encourage cross-team communication, follow Domain-Driven Design (DDD) principles, and implement a solid testing strategy.
By keeping these key points in mind, you can successfully leverage the modular monolith architecture, achieving scalability and maintainability without the complexity often associated with microservices.