Easy Modular Monolith — Part 6 — Synchronous communication between modules

Norbert Dębosz
ITNEXT
Published in
6 min readNov 28, 2021

--

Our modules sometimes have to talk with each other.
This article will demonstrate two ways of synchronous communication between modules using direct references and RESTFull style.

Synchronous communication is a real-time communication, means that we have to wait for a response until going further.

If there is no response or any problem with sending a response back, the caller will raise an exception. This type of communication is typical for client-server applications.

For complex architecture such as Microservices or Modular Monolith, even though that kind of communication is the most simple one and easiest to implement, it has its drawbacks such as potential performance issues or the most critical one as cascade failure (one Module down kills all other in the environment). Still, it is commonly used, so today, we will implement two possible variants in our ModularMonolith.

Context:

In the last article, we added a User Module that stores information about users. Let's imagine that each time we want to save a new product in ProductModule, we would like to keep information on who did that too.

  • The caveat here is that we don't want to duplicate and save whole user details each time we save a new product. (It will be hard to maintain data consistency if someone updates the user's data in UserModule).
  • We don't want to duplicate a User table from UserModule for the same reason.

Instead, we will — in ProductModule — save only the user's global id that executes a method; therefore, we will be able to fetch details later.

Improvements:

Firstly let's improve the UserModule by extending the user class with two new fields, Name and Surname. To do that, we need to inherit from IdentityUser (ASP Core Identity User Class) and then register our new class instead of IdentityUser. Take a look:

Then we should replace all IdentityUser occurrences with a new AppUser class. (I have already done that for you :) ).

Next, let's add a new field to our JWT token — it will store the Global User Id — UserId variable (Line 36).

Having that, let's create a user context class that will allow extracting UserId from JWT Token. This class will be registered in the root Infrastructure project; hence will be available in each Module.

Line 19 — From HtppContext's User context, we extract a claim that we added when logging into the application.

Ok — let's see how to use it:

Firstly, we inject IUserContext — then call a getter UserId.
Very simple — now we have information about UserId that is calling an AddProductCommand. Having the variable, we can pass it to Product.New method.

We good — the piece of information about a user has been saved.

Communication

Now, as we saved info about users in ProductModule, we have to fetch details of the saved user when querying for a product.

Let's create a new project called ModularMonolith.User.Contracts.
The project will store interfaces for methods that UserModule wants to share with other Modules.

Contracts

UserModule then will implement a contract's interface (IUserService or IUserApi) and register it with implementation in WebApi (app entry point). Then injecting it in ProductModule will allow us to access data from UserModule.

Communication between modules

Two ways of communication

Direct communication by reference

Direct communication is communication that will be performed by referencing one project to another. In our case, we will use a runtime reference.

Compile-time reference means that UserModule.Application project would have to be referenced by ProductModule.Application project to be able to fetch data. This reference would create a very ugly coupling and add access to the UserModule queries/command. We won't go this way.

Runtime reference is something that we are using in ModularMonolith all the time. Instead of referencing the whole UserModule, ProducModule will reference only a small and neat UserModule.Contract project. UserModule.Contracts project contains only abstraction over implementation. When we run an application entry point project (WebApi project), it will register implementation; hence it is called a runtime reference — the implementation is not known until the application runtime. It is much cleaner as we share only the methods and DTOs that are necessary in this case.

Let's take a look:

In UserModule.Application we will create a new query that returns user details. What is important (Line 14) this query handler implements our contract interface IUserService.
How as we have our method, let's see how to use it:

Firstly, let's reference a UserModule.Contracts in ProductModule.Application project.
Then let's use our new interface in GetProductQueryHandler:

We inject IUserService into the constructor, then in Line 31, all we do is call a method GetUserDetails, passing a UserId that we saved earlier.

RESTFull communication

If we would like to prepare ModularMonolith for migration to microservices, we would like to use an HTTP request instead of using Direct Communication.
Similar to what we have implemented for Direct Communication in UserModule.Contracts let's create a contract interface:

The most significant difference between this one and IUserService is that:

  • It defines a REST method with a route to a resource.
  • It uses a Refit, a great RESTFull library that allows defining strong typed, interface-based contracts.

As we declared an endpoint that will be used to deliver data, let's create it in our UserController.

Line 36 — as you can see, it is similar to every other method in Controller. What is essential, the method executes GetUserDetailsQuery, which GetUserDetailsQueryHandler will handle. The same one returns data for Direct Communication.
If we scroll up for a moment to GetProductQueryHandler, we will notice that we inject an IUserApi interface there and then call it the same way we did for IUserService.

UserModule Startup

Line 31 — All that we do is point a base URL to our ModularMonolith instance and register IUserApi to request this base URL each time the methods from it execute.

Line 39IUSerSerive implementation registration. This simple line registers a Handler that will be executed in runtime for the GetUserDetails method. If we had more methods in the IUserServcie contract (as right now there is only GetUserDetails), instead of implementing it on the Handler level, we would create a separate UserService class that would handle our cases.

Summary

In this article, I showed two ways of implementing synchronous communication between Modules in Modular-Monolith Architecture.

We should remember that each time we need to communicate with another module, we create a coupling that — if introduced without reason and designed correctly, can bring a lot of troubles in future. That way, if you notice that one Module has to share a lot of data with another, you should consider merging it into one.

Main differences:

IUserService will do an in-process call and return data.
IUserApi will perform an HTTP Get request to a User Controller and execute it.

Which one to choose?

If you are not preparing to migrate into microservices, I recommend sticking to direct communication as it is more straightforward. As an intermediate phase between migration to microservices, I would use HTTP based communication.

There is one more way of communicating that involves changes in how we think about data consistency. In future articles, we will look at asynchronous communication, when and why to use it.

The whole code is available here:

Previous:

In Next Part:

  • Domain events.

In future (this list can change):

  • Asynchronous communication.
  • OutBox improvements.
  • Unit/Integration tests.
  • Storing configuration.
  • The database approaches (multiple data sources).
  • Preparing for Microservices (Replacing MediatR by RabbitMq).
  • Migrate to Microservices.
If my code or articles have been helpful to you, would you mind buying me a coffee?

References:

--

--

Solution Architect | Tech Lead | .Net Developer who searches for the perfect balance between business values and code quality.