Practice : Modular Monoliths
Purpose and Strategic Importance
A Modular Monolith is a design approach where software is built as a single deployable unit but internally divided into well-encapsulated, independent modules. It combines the simplicity of a monolith with the structural discipline of microservices - making it easier to build, evolve, and scale software while avoiding premature distribution.
This approach supports a pragmatic evolution of architecture, enabling teams to maintain high cohesion, reduce coupling, and prepare for future decomposition without incurring the operational overhead of microservices too early.
Description of the Practice
- A single codebase contains multiple modules, each representing a distinct business capability or bounded context.
- Modules are isolated via explicit interfaces and do not directly depend on each other’s internals.
- Internal boundaries are enforced through language features, tooling, or conventions.
- Modules can be tested, built, and reasoned about independently, even if deployed together.
- Refactoring to microservices becomes easier when modularity is in place.
How to Practise It (Playbook)
1. Getting Started
- Identify logical business domains and boundaries within the existing monolith.
- Create explicit modules in code, each owning its data model and use cases.
- Establish interfaces between modules using public contracts or application services.
- Prevent cross-module calls or shared state using compiler rules, dependency management, or peer reviews.
2. Scaling and Maturing
- Align module boundaries with domain ownership and team responsibilities.
- Build automated tests and CI pipelines for individual modules.
- Track and reduce module coupling over time to improve independence.
- Use architecture decision records (ADRs) to capture modular evolution.
- Define policies for versioning, lifecycle, and observability at the module level.
3. Team Behaviours to Encourage
- Treat modules as independently owned units of design and delivery.
- Collaborate across teams to define clear module contracts and responsibilities.
- Consider modular boundaries during backlog refinement, not just implementation.
- Use modular insights to guide decisions about future decomposition or service extraction.
4. Watch Out For…
- Logical modules that are tightly coupled in practice due to shared code or data.
- Poorly defined interfaces that allow accidental dependencies.
- Treating modularisation as a technical goal rather than a business enabler.
- Neglecting testing, ownership, or observability within the monolith.
5. Signals of Success
- Teams deliver and evolve features within their module independently.
- Architecture decisions are reversible and based on usage, not trends.
- Refactoring paths to microservices are visible and achievable.
- Code quality and cohesion improve across the system.
- Delivery speed increases without the need for distributed systems complexity.