Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ


Choose your language

InfoQ Homepage News Design and Implementation of a DDD-Based Modular Monolith

Design and Implementation of a DDD-Based Modular Monolith

Kamil Grzybek recently published a project on GitHub where he has designed, implemented, and in detail described a monolithic application with a Domain-Driven Design (DDD) approach. His goal with the project is to show how a monolithic application can be designed and implemented in a modular way. He also discusses some architectural considerations and design patterns he has found useful in the application.

Grzybek, architect and team leader at ITSG Global in Warsaw, points out that the intent is not to create an oversimplified application or a proof of concept (PoC); he instead describes it as a full implementation that is ready to run in production. His motivation for the project came from having looked for something similar, but without success. For him, most sample applications are too simple or incomplete. Often, he also thinks they are, at least in some parts, poorly designed or implemented, or lack in documentation. 

Grzybek emphasizes that his implementation is just one of many ways to solve the same business problems. The software architecture of a system should be based on a number of factors, such as functional requirements, quality attributes, and technical constraints, but also influenced by developer’s experience, technical constraints, time and budget, and so forth, and all this affects the solution.

The target for his application is the meeting groups domain, which he thinks is a domain most developers can reason about. It also has some complexity which makes it more interesting than a trivial CRUD-based application. He has then divided the main domain into four subdomains: meetings, payments, administration and finally user access.

To find the functionality needed in the domain, Grzybek refers to Event Storming, an approach to exploring complex business domains created by Alberto Brandolini, which he has used to find the behaviour and the events in the different subdomains within the main meetings domain.

From a high level, the architecture defines an API, four modules (corresponding to the four subdomains found) including storage, and a common event bus for communication:

Each module is separated into four submodules, which are implemented as separate binaries: Application for handling all requests, Domain for domain logic, Infrastructure for the implementation of infrastructural code, and IntegrationEvents for the events published on the event bus and which is the only assembly shared between modules. For adding cross-cutting concerns, like Unit of Work and logging, Grzybek is using the Decorator pattern.

Grzybek has separated commands and queries inside the application by using CQRS, and has implemented a variant where queries use raw SQL and views against the same tables used by the commands. There are other variants of CQRS, but he wanted to avoid making the application overly complex.

Integration between the modules is based on events that asynchronously are transferred using the outbox and inbox pattern together with an in-memory event bus as a broker. In the outbox pattern, a separate table is added to the data storage which is used to store events to be published. Adding an event is done by the command performing a task, and in the same transaction as the command. These events are then transferred to the inbox in another module using a separate process; in this project, by a background worker in each module. The implementation provides both at least once delivery and at least once processing. 

Grzybek concludes by emphasizing that the project is still under development and welcomes contributions. The application is a .NET Core application written in C# and uses libraries like Autofac (for IoC) and Dapper (a micro ORM used for read models). The project includes tests based on the Arrange – Act – Assert pattern, implemented using NUnit.

In an interview with InfoQ, Grzybek gave some more insights into his design choices.

InfoQ: How do you compare your monolithic and modular design with a microservices based design?

Kamil Grzybek: The main difference of the modular monolith compared to the microservice architecture is the deployment method and inter-module communication.

In the microservice architecture, each module operates in a separate process. Communication between the modules must be realized using the network and is usually implemented by synchronous service API calls (RPC - Remote Procedure Call) or using a broker (messaging). The microservice architecture is a distributed system - with all pros and cons of this characteristic.

In a modular monolith, we don't have distributed system. All modules operate in the same process and they can communicate without using a network. They can refer directly to objects in memory synchronously by executing methods or asynchronously using some kind of mediator that is still running in the same process.

InfoQ: What advantages and disadvantages do you see using an Eventbus and the Outbox and Inbox patterns, compared to other solutions, like some messaging platform?

Grzybek: The main advantage of the modular monolith is that you do not need any message platform because most of the functionality can be implemented in memory using known design patterns. The modular monolith itself becomes such a platform. Of course, with more advanced systems, the use of a separate platform may be a better solution, but such a decision must be made with caution.

InfoQ: You have implemented the CQRS pattern using views and raw SQL instead of separate tables. Can you elaborate on that?

Grzybek: My approach is the simplest CQRS implementation, but powerful and often far enough. Using view is a form of an abstraction and contract between application and database. You can always go to the next level of CQRS implementation and reading application logic can be left untouched.

The next level is materializing views, which causes a slight reduction in write performance to speed up reading. The most advanced systems that need to scale very well introduce the highest level of implementation of CQRS pattern and update the reading model asynchronously. It introduces Eventual Consistency.

Each level of implementation complicates our solution more, so we should move to a higher level only when we really need it.

Rate this Article