Scalable Modular Monolith
Microservices are a popular architectural style for building scalable and maintainable applications. However, they also introduce complexity and overhead. Microservices are great for large applications with many teams working on different parts of the application, but for smaller applications and teams, a modular monolith can be a better choice.
Below is a comparison between microservices and modular monoliths for small teams and applications.
| Aspect | Microservices | Modular Monolith |
|---|---|---|
| Deployment | Multiple services to manage, version, and deploy independently | Single artifact; simpler deployment process |
| Maintainability in Small Teams | High overhead; teams need expertise across infrastructure and DevOps | Better fit; easier to understand and maintain single codebase |
| API Spec Coordination | Requires formal API contracts and documentation; changes need careful versioning | Direct code coordination; changes are immediately visible across modules |
| Versioning Between Dependencies | Complex vesioning; services must support multiple API versions | Easier to refactor; all modules updated together in same release |
Modular Monolith
When starting a new project, it can be beneficial to start with a modular monolith architecture. This allows you to build a single application that is organized into modules, which can be developed and tested independently. As the application grows, you can then refactor it into microservices as needed.
Horizontal Scale
To make the monolith horizontally scalable make sure it is stateless. This means that the application should not rely on any in-memory state, and should instead use a database or other external storage for all stateful data. This allows you to easily scale the application horizontally by adding more instances, and also makes it easier to refactor into microservices later on. The statefull services such as databases can be scaled independently of the application.
Feature Flags
All functionality is in onne artefact which makes deployments simpler. You can also use feature flags to enable or disable features as needed, which allows you to deploy new features without affecting the entire application. Environment variables can be used to control the features.
Modes of operation
Similar to feature flags, you can also have different modes of operation. For example, using an environment variable to run application in db migration mode, where the application will only run database migrations and then exit. This can be useful for performing maintenance tasks as jobs or running the application to only serve a specific function.
Coordination between dependencies
Module dependencies are easier to manage in a monolith, as all modules are part of the same codebase. This allows you to easily coordinate changes across modules, and also makes it easier to refactor code as needed. In a microservices architecture, coordinating changes across services can be more complex, as each service may have its own codebase and deployment pipeline.
Module Structure
Below is package structure for a modular monolith application. Each module is organized into its own package in java, and the main application is responsible for coordinating the modules.
com.example.app
├── main
│ ├── Application.java
│ └── config
│ └── AppConfig.java
├── module1
│ ├── Module1Service.java
│ └── Module1Controller.java
├── module2
│ ├── Module2Service.java
│ └── Module2Controller.java
└── module3
├── Module3Service.java
└── Module3Controller.java
Deployment
The diagram below shows how such a monolith can be deployed in a scalable way.
-
Loadbalancers can be configured to only route traffic to instances with label: “HandleRequests=true”. This allows instances to be used for different purposes.
-
Environment variable such as Mode is used to control the mode of operation for the application. For example, you can have a mode performing maintenance tasks only such as Migrating the db.