81. Modular Monolith Architecture
Status: Accepted Date: 2025-07-06
Context
We need to choose an architectural style for the Mercury backend. A traditional, tightly-coupled monolith can become difficult to maintain as it grows. A full microservices architecture introduces significant operational complexity (service discovery, distributed transactions, complex deployment pipelines) which is a heavy burden for a small team. We want the development benefits of clear separation of concerns without the operational overhead of distributed systems.
Decision
We will adopt a Modular Monolith (or "microservices within a monolith") architectural style.
The application will be developed as a collection of highly modular, loosely-coupled components (e.g., Apollo, Dike), each with a well-defined public interface and a private implementation. These modules will be organized as separate directories within our monorepo and will communicate with each other through their public APIs (TypeScript interfaces) via dependency injection.
However, at deployment time, all of these modules will be packaged and deployed together as a single application process. We get the logical separation of microservices at development time, with the operational simplicity of a monolith at deployment time.
Consequences
Positive:
- Development-Time Separation: Enforces strong boundaries between modules, preventing developers from creating "spaghetti code" and encouraging a clean, maintainable structure.
- Deployment Simplicity: We only have one application to deploy, monitor, and scale. This drastically reduces operational complexity compared to a microservices architecture.
- High Performance: Communication between modules happens via in-memory function calls, which is orders of magnitude faster and more reliable than network calls between microservices.
- Transactional Integrity: We can use standard database transactions that span multiple modules without needing complex distributed transaction patterns like sagas.
- Future-Proof: This architecture provides a clear path to evolving towards microservices in the future if needed. A well-defined module is a natural candidate to be extracted into its own service.
Negative:
- No Independent Scaling: We cannot scale individual modules independently. If the
Apollomodule is under heavy load, we must scale the entire monolith, which is less resource-efficient. - Tightly Coupled Deployment: All modules are deployed together. A change in any module requires a redeployment of the entire application. A bug in one module can potentially crash the entire application.
- No Technology Diversity: All modules must be written in the same language and framework (TypeScript and NestJS).
Mitigation:
- Appropriate for Current Scale: For our current team size and performance requirements, the benefits of operational simplicity far outweigh the need for independent scaling.
- High Availability Deployment: The monolith will be deployed as multiple replicated instances behind a load balancer. If one instance crashes, traffic is routed to the others, ensuring high availability.
- Strong Module Boundaries: We will enforce strict rules about module communication. Modules can only depend on the public interfaces of other modules, never on their implementation details. This will be enforced via code reviews and potentially linting rules.
- Unified Tech Stack as a Benefit: Having a single, unified technology stack simplifies hiring, development, and tooling.