DEV Community

Cover image for Understanding the potential of Modulith architecture
Max Beckers
Max Beckers

Posted on

Understanding the potential of Modulith architecture

In software architecture, it is always about finding the best solution for your use case and your application in your specific context. Based on that, you have the challenge to find the right balance between flexibility and simplicity. We often must decide between a monolithic system and a microservice architecture. For a long time, the monolithic architecture has been the way to go. Then, microservices came up and most architectures followed way. This blogpost is about the relatively new architecture approach of modulith that finds its place in-between the other two approaches and tries to combine their benefits.

Monoliths: The focus is Simplicity

A monolithic system, with its integrated codebase and unified structure, offers a simplicity that greatly streamlines development processes. Developers find solace in the cohesive nature of a monolith, where maintaining a singular codebase eliminates the complexities associated with distributed systems. Deployment becomes a straightforward task, as there’s no need to manage the intricacies of coordinating multiple services across a network. Furthermore, the absence of network handling concerns, error propagation, bandwidth issues, and circuit breakers simplifies the development landscape, allowing teams to focus on building features rather than navigating the intricacies of distributed system challenges. While microservices tout their advantages, the monolithic approach stands as a testament to the elegance of consolidated, efficient development practices.

Image description

Pros of Monolithic Systems:

  • Simplicity: With monolithic architectures, you have a single codebase for all the applications, which simplifies the development, debugging, testing and the deployment process because you do not have to manage multiple services.
  • Development Productivity: Developing monoliths is easier because they are tightly integrated, allowing developers to concentrate on different parts of the application without worrying about the interactions.
  • Performance: The benefits of monolithic architectures over distributed architectures come from the fact that inter-service communication does not cause any network overhead.
  • Transaction Handling: Transaction handling of different (database) operations is easily possible.

Cons of Monolithic Systems:

  • Maintainability: When a monolithic architecture grows, it can become unwieldy and hard to maintain, making changes or the introduction of new features is challenging and risky. This often results in unintended consequences or regressions, as well as making it difficult for new developers to learn the entire codebase. Looking at the relations between the different classes of the application can become a “big ball of mud” over time.
  • Technology Stack Limitations: Monolithic systems often require a uniform technology stack for the entire application. This can limit the ability to leverage the best tools or frameworks for specific components or functionalities of the application. Having a monolith in different languages makes it even more difficult to maintain and deploy it.
  • Deployment Complexity: Deploying a monolithic system requires deploying the entire application, it’s not possible to deploy just a part or a feature. This can increase deployment complexity and limit the ability to deploy individual components independently. At the beginning it might be easy to deploy the monolith, but with time the complexity can increase. The overview what changes over each release has to be tracked, especially when there are multiple teams working on the monolith.
  • Scalability: Monolithic applications can be challenging to scale because the entire application has to scale. Even if only one part of the monolith gets high traffic, the whole monolith has to scale and this requires a lot of resources. Scaling all components, even those with low traffic, can result in unnecessary costs and resource consumption.
  • Testing Challenges: Testing a monolithic system can be more challenging due to the tight coupling between components. Changes in one part of the application may have unintended consequences on other parts, making it necessary to perform comprehensive regression testing.
  • Single Point of Failure: In a monolithic system, a failure in one component can bring down the full application. There is no isolation between components, increasing the risk of cascading failures.

Microservices: Scalability is in the focus

Microservices gained popularity as an alternative to monoliths, leveraging the concept of breaking down applications into small, independent services. Each microservice focuses on a specific functionality and communicates with other services through well-defined interfaces. In a microservice architecture the development teams are often designed to run there applications in the you build it - you run it way. This is stated inConway’s Law where the teams and the organisation must match with the software. Some benefits of microservices include:

  • Scalability: Microservices offer granular scalability, allowing specific services to scale independently based on demand. This enables efficient resource allocation and cost optimization.
  • Technological Diversity: Microservices provide the flexibility to use different technologies for each service, depending on its requirements. Developers can choose the most appropriate technology stack, programming language, or framework for each service, optimizing performance and development productivity.
  • Flexibility: Development teams can work independently on different services, using different technologies or languages, allowing greater innovation, and reducing the time-to-market.
  • Maintainability: Each microservice has a low level of coupling to the other services, making the services itself and the overall system easier to understand, modify, and maintain. Changes or updates to one service have minimal impact on others, reducing the risk of cascading failures and facilitating iterative development.
  • Reusability: In a microservice architecture reusability is the key and one of the main concepts for microservices. Each microservice can be used by other applications / services.

However, microservices also come with challenges (for this see also the Microservice Prerequisites by Martin Fowler):

  • Complexity: Microservices come with inherent complexity. Developers must handle service discovery, inter-service communication, and data consistency between services. Building robust distributed systems requires careful design and management of these complexities.
  • Orchestration Complexity: Orchestrating and deploying multiple microservices is one of the major concerns in microservices architectures. The resulting operational complexity requires additional infrastructure and monitoring, such as service discovery, load balancing, and fault tolerance.
  • Challenges of Distributed Systems: Microservices rely on network communication to interact with each other. Latency, network failures, and potential failure points are common risks as a result. Data consistency, service discovery, load balancing, and fault tolerance can also be challenging to implement.
  • Transaction Handling: Managing transactions in a distributed system is inherently complex due to operations occurring across various services. One approach involves setting a defined timeframe for receiving asynchronous responses to events. In case of a rollback, each service must provide a dedicated endpoint to facilitate the process seamlessly.

Image description

Modulith: Find the balance for your application

Modulith combines the best of both worlds, offering a balanced approach that mitigates the drawbacks of monoliths and microservices. Modulith is a modular monolith, where the application is divided into loosely coupled modules or domains. Each module represents a distinct area of functionality and can be developed and tested independently.

When you break the Modulith into different deployable modules, it can be done using different frameworks or programming languages, allowing you to scale them independently. In addition, you can build the modules in such a way that they can be scaled. But before you invest time into such architecture, check if you have really need to scale the modules individually. In most cases, this type of flexibility is not required for many systems, as in most cases all parts of the application need to scale in the same dimension as traffic increases. This means that the whole Modulith can be scaled up in most cases when the traffic increases and after reaching the peak it can be scaled down as one application to reduce the costs.

Here’s why Moduliths are gaining popularity:

  • Simplicity and Maintainability: Moduliths retain the simplicity and maintainability of monoliths by encapsulating the entire application within a single codebase. Developers can easily navigate the codebase and make changes without the need for complex inter-service communication.
  • Clear Boundaries: Modulith emphasizes clear boundaries between modules, ensuring separation of concerns and loose coupling. This makes it easier to identify about dependencies and maintain a modular architecture without the operational complexities of microservices.
  • Monolith alike performance: Because of reducing the network communication, the Moduliths can run its modules in the same processes or on the same hardware and can benefit from this direct usage of the modules.
  • Evolutionary Approach: Moduliths offer an evolutionary path from a monolithic architecture. The advantage of starting with a modular monolith is that developers can gradually extract modules into independent microservices as the need arises, making it easier to transition to a microservices architecture as needed.
  • Easier Deployment: Moduliths are assembled into one deployment unit, which reduces the coordination for deployments. There is no need to ensure that the other modules are deployed in the correct version as it is needed in a microservice architecture.
  • Less complexity: Compared with microservices it is easier to configure Moduliths and the overhead for development is reduced. Compared to a monolith, the complexity to understand the code and to test the separated modules of Moduliths is lower. The complexity of the business logic itself stays of course, but when it is less challenging to identify the different parts of the application and have a clean structure and clear boundaries, it is easier to understand the system.
  • Cost Effective: Moduliths prove to be a cost-effective choice by consolidating modular functionalities within a single codebase, eliminating the need for complex infrastructure setups and intricate network configurations, including the management of TLS. This streamlined approach not only reduces economic overhead but also enhances development efficiency, making moduliths a pragmatic solution for optimizing both cost and operational effectiveness in software architecture. Amazon was able to reduce the costs of one of their services by 90%.
  • Scalability: Moduliths provide a scalable architecture by allowing individual modules to be scaled independently. Modules that experience higher demand can be scaled without impacting other modules, achieving better resource utilization. For that of course the modules must be encapsulated and designed to be scaled individually.
  • Flexibility: Moduliths allow for technological diversity within modules, like microservices. Developers can choose the most suitable technology stack for each module, optimizing performance and leveraging the strengths of different frameworks or programming languages. But this technology flexibility might bring up a higher complexity to the application than just having it in one technology stack.

The two points of scalability and technical flexibility may stand out somewhat from the above list. This is also the reason why I have placed these two points at the end of the list. Both of these points naturally bring further complexity with them. These should be avoided as far as possible if they are not necessary. Nevertheless, I would like to include these points here and explain them a little to make them easier to understand.

Scalability is about processing the load as well as possible when it occurs. Since in a modular application you want to avoid the network traffic that you have in a microservice architecture, multithreading often comes into play at this point. A relatively simple option at this point is the processing of queues. It often makes sense to outsource everything that is not required for synchronous processing to an asynchronous process via a message queue or similar. For asynchronous processing, the number of consumers can be scaled up relatively easily in most frameworks. We already have a module that processes the events at scale. But there are also ways of scaling a module by implementing a kind of load balancer that starts the module in several threads and then distributes the requests to the module to different threads, e.g. via round robin. This second approach is of course much more complex to implement, but it can help you to optimize the traffic generated by frequently used modules or those with high demand computing power.

Having several technologies in one codebase and in one modulith is also something that should only be implemented if there are really good reasons. Here too, however, the performance of individual modules can be the driver. I would also like to provide a few examples here to improve understanding of when it might make sense. A relatively simple example could be if you have a modular application that is written in Java and you have a part for file generation that can be implemented more easily and with better performance in Python. It is then conceivable to implement this module for file generation in Python as a module and to use this outsourced module from within the application. For example, you could build the Python tool as a CLI application and then call it from Java. However, you can also consider whether you want to use the same programming language for synchronous processing and asynchronous handling of individual events or whether in certain scenarios it makes sense to write the majority of the application in Java, but to outsource the processing of events for sending emails to a module in PHP, for example.

These are just a few small examples of the two areas to make it a little clearer and show the possibilities. Each of you may have further ideas or other examples in mind. It all depends on the system and the application.

Break the Monolith

Another use-case for a Modulith can be the process to break a monolith into microservices. To split a monolith up into microservices can be a good way to increase the maintainability of the software and to benefit from a microservice architecture. However, this approach can be very hard. It might be helpful to use the Modulith as a guide to break the monolith into microservices. It is helpful to separate the logic of the monolith into different modules and to reduce the coupling of the different parts of the code by defining APIs for the modules. This APIs might be interfaces in the first step. You still have one repository for the code, but you can profit from the benefits like simplicity and maintenance of the system. And perhaps you already terminate with your refactoring when you have a modular monolith, because you don’t need the flexibility of a microservice architecture. Otherwise, you go further and split up the modules into microservices.

A very important but also very hard part of the refactoring is the database. In a Modulith each module should have its own database (at least its own schema on the same database server) similarity to microservice. It is not recommended to have an integration of the different modules via database. For existing and long running monolithic applications where each part of the code is directly using the database this will certainly be the most complicated part to define APIs between the modules and to ensure that each module gets its own data.

In order to split the core of the monolith into modules, you must identify the domains of your application and you need to define the bounded contexts. The reason for this is that most of the monolithic applications were not build with a clean defined domain like Domain-driven Design does.

If you use Java and Spring, you can have a look at Spring Modulith. It is an experimental Project by Spring to build modular Monoliths with Spring. This project can help to encapsulate your modules and to find a good project structure. For example it is helpful that references to internal module packages (sub-packages) are rejected.

Module design - a dive into module

The purpose of this section is to demonstrate how to begin creating modules for the modular monolith. You need to define the modules first, and based on that, the APIs that need to be used. This can consist of interfaces and data transfer objects (DTOs) or as well specific services that are allowed to be called from a context out of the module. It is helpful to have the rest of the code in the module in another namespace or have one module for the API and one for the implementation behind the API. I have used both ways and which way is better depends a bit on the context. Mostly it might be the best way to start with one module that includes the API and an internal namespace for the module’s internal classes.

In a multi-module maven project for example, a simplified hierarchy could look like the illustration below. But this can be adapted to your framework and programming language. It could also be done in one maven module and separating by the namespace, but the multi module way it is more explicit, and it is ensured that you only use classes from modules you added to your pom file.

Image description

In a monolithic architecture we would not have such an internal namespace. There, each class would be able and allowed to call any public function or class from another namespace. In addition to that, the codebase is structured in namespaces and not separated in well defined modules.

For testing you can of course define unit tests for the module’s internal logic and classes. Furthermore it is useful to tests the module API and view the rest of the application as a black box. This kind of integration tests (grey box tests) for your application are easier to run, maintain and implement than in a microservice environment where each called microservice also must run.

Practical Example

To illustrate the concept, let’s delve into a real-world example focusing on a Company Management System. This system boasts a modular architecture reminiscent of a puzzle, with a central Core serving as the foundation. Imagine the Core as the backbone, comprising various individual modules such as User Management, Company Structure, Portal with an Administration Interface, and more. While the following diagram simplifies the structure, it effectively conveys the fundamental idea.

Image description

The application revolves around the Core, which provides interfaces for seamless integration of additional modules. Each module acts like a piece of the puzzle, bringing its unique functionalities to the system. For instance, a module could introduce its own user data, widgets for the portal, or even features for the administration interface. Essentially, this system operates as a foundational framework, akin to a construction kit. Users can leverage existing interfaces to deploy and expand functionality as needed.

Consider the scenario of integrating a new module, such as a Job Application Workflow or Time Tracking module. This process is straightforward and independent of other modules, thanks to the well-defined interfaces and the Core’s existing infrastructure. Whether it is adding new features or extending existing ones, the system allows for modular enhancements, providing a flexible and scalable solution.

While it is conceivable to expose the interfaces as HTTP endpoints and adopt a Microservices architecture, the chosen approach aligns with the concept of a Modulith. Given the manageable traffic, even with several thousand users, this design choice prioritizes simplicity and cohesion, making the system efficient and adaptable to evolving business needs.

Conclusion

Modulith presents a compelling architectural alternative that combines the simplicity of monoliths with the flexibility of microservices. By providing clear module boundaries and enabling independent development and deployment, Modulith comes with a balance that suits many applications’ needs. It allows for scalability, maintainability, and technological diversity while avoiding the operational complexities of a full-fledged microservices architecture. As software systems continue to evolve, Modulith proves to be an attractive choice for building adaptable and efficient applications.

Top comments (0)