
Modulith structure

There are many ways to create a Modulith, and I will introduce you to one of them. The important parts that we will discuss are: Application layer, Business modules, Facades / Repositories, Event bus/broker, CQ Router and Utilities.
Understanding the roles of these parts is essential to building a code base that won't end up as a "big ball of mud".
You can find modulith introduction in another article.
Application Layer
The application layer primarily accepts requests. One or more entry points can be exposed by the application. The execution of the application can be done via HTTP port or any other protocol or from the console. The application can also be hooked to a queue, etc.
The application layer translates requests and responses. It translates requests from the entry point API to internal structures that can be passed to modules. It also translates module responses from internal structures to the entry point API. Nothing outside the application layer should know how it was executed.
The application layer may contain the following functionalities:
- Entry point
- API validator
- API translator
- Middleware
- Router
- Sets up logging
- etc.
The application layer should not contain business logic (business logic adds value for customers). Although middleware can contain authentication and authorization, these are not business logic per se.
Business modules
Modules handle the business logic of your application, which adds value for your customers, both internal and external. Each module should have its own API/interface and only one way to access it. Imagine a class that exposes all the module's functionality, with no other way to access anything within the module.
The point is to be able to alter the code of a module without worrying about what could break. A module represents another layer of abstraction. Implementation details are kept private, and only the necessary functionality is exposed.
Each module owns its own data, such as database schemas and object stores. Modules are solely responsible for managing their own data and defining its structure. One module can only access another module's data through its interface.
The module API/interface should use business terminology, not implementation terminology. E.g. isUserLoggedIn(token: string)
instead of tokenExists(token: string)
.
Facades / Repositories
A facade is an implementation of a third-party API (external or from another module) that meets your application's needs.
I've placed the repository here because it's just a version of the facade.
The third-party API should be separated from your application and should only be called through the facade. This reduces the coupling between your application and the API that you do not/should not have control over. Essentially, you are protecting your business logic from anything that is not your business logic code.
As you can see in the diagram, the business logic is protected from the external API, the storage API, the other modules' APIs, and the application layer (through the module interface). The facade pattern can absorb many changes in external parts from the module's perspective.
Event bus/broker
Here, I'm talking about business events (UserCreated
), not technological events (SocketOpened
).
Events should be an integral part of your application. Events can significantly reduce coupling among modules. Imagine all the cases in which you need something to happen, but you are not interested in the result.
- Send an email after a user registers or interacts with the site.
- Notify the store department about an order.
- Update the project status once all the tasks have been completed.
- Notify Accounting that the payment has been made.
- Increase the number of views.
All of these can exist in the middle of another process that is not dependent on the outcome of these examples. Rather than calling a different module (coupling), you can raise an event and allow other modules to listen to it (decoupling).
The event bus/broker provides the technology for transferring events. Ideally, there should be just one so that modules do not have to decide where to listen to events, although there is a case to be made for having more.
The event bus/broker should not contain any business logic. Personally, I prefer subscription to specific events by module instead of bus/broker routing events. This approach avoids putting business logic in yet another place.
CQ Router
But what if you want to call another module and care about the result? What if you need a basic level of business logic? That's what the CQ Router (command/query router) is for.
Although you can create a facade to communicate with another module, CQ Router creates an additional level of decoupling and abstraction between modules.
At the same time, this component is ideal for patterns that require multiple modules to function, such as rules engines, scatter/gather, and pipelines.
CQ Router functions should be kept simple and should not replace orchestrator/job modules.
Utilities
Well, utilities provide utilities. The important thing here is to understand what a utility is. A utility is a business-level agnostic extension of language.
Imagine that you are writing a mailing application. The functionality that decides the sending of campaigns is business-level logic and belongs to the module. The functionality of creating a MIME from an email object is independent of your login and can be a utility functionality.
Since utilities are "language extensions," they can be called from anywhere, just like functions in a programming language.