From Monolith to Microservices: A Gradual Approach with Modular Monolithic Architecture

As software developers, we are always looking for better ways to build applications that are stable, maintainable, and scalable. In this blog post, I will share my experience with using the Modular Monolithic architecture.

What is Modular Monolithic?

Modular Monolithic architecture is an architectural pattern where the application is divided into separate modules that work together as a single, cohesive unit. In this pattern, each module is responsible for a specific set of features and has well-defined interfaces for communicating with other modules. Modules should be created based on business domains to ensure that each module has a clear and specific responsibility.

When to Use Modular Monolithic Architecture

Modular Monolithic can be a good choice for quickly getting a project off the ground and adapting to changing business requirements. By starting with a modular monolithic architecture, it is possible to gradually move towards a microservices architecture without the need for a complete rewrite of the codebase.

When building a new system, it is important to focus on understanding the business domain first. You need to identify the business capabilities, processes, and data that the system needs to support. Once you have a clear understanding of the business domain, you can then start thinking about how to partition the system into individual services.

If you are not sure about the domain boundaries, you may want to start with a monolithic architecture and gradually move towards a microservices architecture as you gain more knowledge and experience. A monolithic architecture allows you to build the system as a single, cohesive unit, and it can be easier to refactor and break down into microservices later on. This approach can help you to identify the domain boundaries and dependencies between modules, which can then be used to guide the partitioning of the system into microservices.

Interactions between Modules

It is essential to decide on a consistent pattern for how modules should interact with each other. Otherwise, the lack of standardization may lead to code that is difficult to maintain.

In general, there are several common patterns for module interaction:

Function calls:
One module calls functions or methods in another module to request some operation or data. However, this can lead to tight coupling, where one module relies heavily on another module’s functions or methods, leading to cascading effects on the calling module. Depending on the number of function calls, the performance of the system may be affected due to the overhead of the function calls.

Events:
One module publishes events to a message bus or event stream, and other modules subscribe to these events to react to them. However, when modules rely on events, there may be a lag between the time an event is generated and the time that other modules receive and process the event, leading to eventual consistency issues.

HTTP APIs:
Modules expose HTTP APIs that other modules can use to request data or trigger operations. However, since HTTP APIs rely on network calls, the latency of the system may be affected, leading to performance issues. As the system evolves, the APIs may need to change, leading to versioning challenges and backward compatibility issues.

Databases in the Context of Modular Monolithic

It is essential to avoid allowing data owned by one module to be directly accessed by other modules. If this is not done correctly, we might end up creating a monolithic architecture.
In general, we have two patterns for managing persistence state in Modular Monolithic architecture:

Database per module:
Each module has its database schema and interacts with it directly. This approach provides isolation between modules and can simplify data management.

Database per bounded context:
Each module has its database schema, but the schemas are organized by bounded contexts, which represent different areas of the system’s business domain. This pattern is common

When to extract a module as a separate service:

While modular monolithic architecture provides a flexible and adaptable approach to building applications, there may come a time when one or more modules outgrow the monolithic architecture and need to be extracted as separate services. One common reason for this is when a module becomes too large and requires more resources to scale than can be efficiently handled within the monolithic architecture.

To determine if a module should be extracted as a separate service, consider the following:

  1. Is the module experiencing performance issues due to its size or resource requirements?
  2. Are there other modules in the system that depend heavily on the module in question?
  3. Are there clear boundaries around the module’s functionality and business domain that make it suitable for extraction as a separate service?
  4. Are there benefits to extracting the module as a separate service, such as improved scalability, fault tolerance, or the ability to independently deploy and maintain the module?
Share Comments