Cross module communication in modular monolith
Asked Answered
D

4

12

I have been learning about modular monolith project structure in this article: https://codewithmukesh.com/blog/modular-architecture-in-aspnet-core

Most of it makes sense to me but something I don't quite get is:

Cross Module communication can happen only via Interfaces/events/in-memory bus. Cross Module DB Writes should be kept minimal or avoided completely.

How exactly does that cross-module communication look?

Let's say I have 3 modules:

  • Product
  • User
  • Security

My security module registers an endpoint for DisableUser. It's this endpoint's job to update a User and every Product associated with the user with a disabled status.

How does the Security module call User & Product update status method in a unit of work?

My understanding is that this pattern is intended to make it easier to extract a module to a microservice at a later date so I guess having it as a task of some sort makes it easier to change to a message broker but I am just not sure how this is supposed to look.

My example is obviously contrived, my main point is how do modules communicate together when read/writes are involved?

Denitrify answered 7/6, 2022 at 20:10 Comment(2)
The meaning (my interpretation) is that communication isn't going to be going through a message broker like Kafka. Instead, you would define messages you want to subscribe to in a shared project for the individual modules to register to. This could be done via a traditional event or multicast delegate, or if you're using MediatR as the project suggests you would define some interface IMyEventNofitication : INotificationHandler<MyEvent> in the shared project, and implement it with your logic in each module that wants to subscribe to the event. You then Publish said event through MediatR.Riordan
Just to add to the above: While MediatR encourages command-query separation, it does not directly enforce it. In cases like this, an INotificationHandler likely should be considered a Command, and therefore should only be registered by Commands that would dispatch to it assuming the intention is to change state. If using this infrastructure you at some point decide to transition to microservices, you would redefine the notification handlers to instead push to your message broker to be picked up by the other services in place, and have those messages consumed by the other relevant services.Riordan
I
19

Theory

There are lot of misunderstandings about terminology in such questions, so let's mark 2 completely different architectures - monolith architecture and microservices architecture. So one architecture that stands between these both is a modular monolith architecture.

Monolith architecture mostly has a huge problem - high coupling and low cohesion because you have no strong methods to avoid it. So programmers decide to think about new ways of building different architectures to make really hard to fall down in high coupling low cohesion problem.

Microservices architecture was a solution (despite other problems it solve too). Main point in microservices architecture is all about separation services from each other to avoid high coupling (because it is not so easy to setup communication between services as in monolith architecture).

But programmers can't move from one architecture to completely different in "one click", so one (but not only one) way to build microservices architecture from monolith architecture is to make modular monolith first (just solve high coupling low cohesion problem but in monolith) and then extract modules to microservices easily.

Communication

To made coupling low we should focus on communication between services. Lets work with sample you put in your question.

Imagine we have this monolith architecture: Monolith architecture

We definitely see high coupling problem here. Let's say we want to build it more modular. To make that, we need to add something between modules to separate them from each other, also we want modules to communicate, so the only thing we must to add is a bus.

Something like that: enter image description here

P.S. Is could be completely separated not im-memory bus (like kafka or rabbitmq)

So your main question was about how to make communication between modules, there are few ways to do that.

Communication via interfaces (synchronous way)

Modules could call each other directly (synchronously) through interfaces. Interface is an abstraction, so we don't know what stands behind that interface. It could be mock or real working module. It means that one module doesn't know nothing about other modules, it knows only about some interfaces it communicate with.

public interface ISecurityModule { }
public interface IUserModule { }
public interface IProfileModule { }

public class SecurityModule : ISecurityModule
{
    public SecurityModule(IUserModule userModule) { } // Does not know about UserModule class directly
}

public class UserModule : IUserModule
{
    public UserModule(IProfileModule profileModule) { } // Does not know about ProfileModule class directly
}

public class ProfileModule : IProfileModule
{
    public ProfileModule(ISecurityModule securityModule) { } // Does not know about SecurityModule class directly
}

You can communicate between interfaces through methods call with no doubt but this solution doesn't help well to solve high coupling problem.

Communication via bus (asynchronous way)

Bus is a better way to build communication between modules because it forces you use Events/Messages/Commands to make communication. You can't use methods call directly anymore.

To achieve that you should use some bus (separated or in-memory library). I recommend to check other questions (like this) to find proper way to build such communication for your architecture.

But be aware - using bus you make communication between modules asynchronous, so it forces you to rewrite inner module behaviour to support such communication way.

About your example with DisableUser endpoint. SecurityModule could just send command/event/message in bus that user was disabled in security module - so other services could handle this command/event/message and "disable" it using current module logic.

What's next

Next is a microservice architecture with completely separated services communicating through separated bus with separated databases too: enter image description here

Example

Not long time ago I've done project completely in microservices architecture after course.
Check it here if you need good microservices architecture example.

Images were created using Excalidraw

Interrex answered 18/6, 2022 at 8:46 Comment(4)
Thanks for your response. When building the modular monolith approach would an in-memory bus be appropriate? To me it seems so. It doesn't slow things down with serialization and would make it easier to swap to a persistent message bus if you convert the module into a microservice. I ask this because everywhere in-memory message busses are discussed they point out it's not for production but I assume they are referring to microservice structure and not a monolith.Denitrify
@Denitrify I don't see any drawbacks with using in-memory bus in production if you have monolith, but, again, it surely depends on architecture of your monolith. You should think about scalability, errors handling, sync/async communication between modules, but as a step to build microservices in the future - it is a good step.Interrex
its also worth to mention that the async communication pattern have an impact on consistency of data arround the whole application. Data get into an eventual consistent state due to the async nature of message handling.Stuppy
Regarding the different approaches to deal with communication, it is not one to choose, each use case might require a different approach, if it can have eventual consistency then event is fine, however if it is a critical process then maybe sync process is the best, while having everything in a unit of work is almost impossible, think that one benefit of modular monolith is to turn modules into external services in the future, so keeping opened transactions for external cross-module calls might not be a good idea.Olden
M
5

First glance, I think one approach is to use Mediator events since the project already uses that. It would work well and keep everything separate.

To define Mediator event check this.
You define your events in shared core, for your example:

public class UserDisabled : INotification
{
    public string UserId { get; set; }
}

From the User modules you will publish the event when the user get disabled

await mediator.Publish(new UserDisabled{UserId = "Your userId"});

And Finally declare event handlers in every modules that need to react to the event

public class UserDisabledHandler : INotificationHandler<UserDisabled>
{
    public UserDisabledHandler()
    {
        //You can use depency injection here
    }
    public Task Handle(UserDisabled notification, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

However it is worth noting that this won't work if you want to switch to actual micro-services. I'm not very familiar with micro services, but I think you need some form of event bus and that's where micro-services becomes complicated.
There is information about that in this Microsoft book.

Musca answered 14/6, 2022 at 23:32 Comment(1)
definitely mediator is the best pattern to put in place and MediatR is the best .net library that implements this pattern. When switching to microservices you need to define how you want your communications to be like. You have 2 choices: - Synchronous communication: your User microservice will need to have a "DisableUser" Rest (or similar) endpoint that you Product Microservice will call - Asynchronous communication: you need a message handler / message queue like Apache Kafka, Rabbit MQ or similar that will propagate the messages from one microservice to anotherLumbricalis
H
3

Side note about overall example

!!! Scroll down if interested only in specific approach for communication between modules.

I find the whole example and architecture problematic when all modules sound like these "modules" are just repositories around some entity, e. g. "User" module is actually a repository for entity "User" and nothing more.

To my taste, User is one of entities related to Security module. They belong to same domain, after all.

I am not sure why you need to somehow store "product disabled for user X", but if "User account is disabled", then thing that check access to products, Security module, I guess, would just reply that product is disabled, without checking for per-product flag?

So, may be, it all should happen in Security module, as other modules would ask it whether user X can use product Y?

Communication between modules

Modules, logically in monolith and "physically" for microservices, are self-contained.

So they are must be designed in a way that only module's public APIs can be called outside of a module. If module A can call some internal thing of module B in modular monolith, it will become a disaster eventually.

Approaches

Actually, there are just 2 approaches for communication between modules:

  1. events - messages sent to module for processing in "throw and forget manner", without waiting for the response.
  2. commands - requests sent to module with expectation of "synchronous" processing and obtaining a result for the processing.

Implementations may vary - from some in-memory events bus and some in-memory commands dispatcher, to Kafka for events and http(s) requests for commands.

Homager answered 29/3, 2023 at 3:45 Comment(0)
M
3

All the answers here are good for their attempts to deeply explain things.

What I am not satisfied with is that everyone discusses new shiny term - "modular monolith" not making their homework at first.

All the solutions people discuss in the context were introduced very long ago.

And professionals in our field used them since.

Event bus (pub/sub, observer): 1970s - Smalltalk, 1990s - Gang of four. SOA (which is service isolation, modularization, in turn is SoC (1970s) and SRP (2000s) in a nutshell) - late 1990s. DDD blue book - 2003. Hexagonal Architecture - 2005.

You do not need "modular monolith". As well as a "monolith" is nothing close to being an equivalenl to "big ball of mud".

In minimal monolith:

  • isolate subdomains domain logic under the single contact point - Domain Service (s); this is the only point of access to subdomain operations;
  • code as many subdomains as the domain requires;
  • for sinchronous communications call subdomain A domain service from subdomain B domain service;
  • for asynchronous communications use event bus(es) (global, local, at will) again from domain services;
  • use ports/adapters incoming/outgoing to wrap communications with external (to a subdomain) things (HTTP, DB, email, Kafka, Rabbit etc.);
  • deploy as a single unit, isolate teams on subdomains;

This is minimal professionally architected monolith. It does not have to, must not be the "big ball of mud" way.

And lastly, "modular monolith" should assume modules are deployed independently, thoug it did not sound in answers. With this in mind it becomes just separate isolated services - not that nonexistent "modular monolith" - with their own DBs (where needed). Again they do not need to be "micro" - remember SOA and RPC have been available to all professionals for decades.

I do not want to hear like "SOA is awful" and the like. Projessionals make decoupled, extendable, reliable, easily maintainable software with a stick and a rope because they care to know they profession inside out. Instead passers by make big ball of mud of everything, posers "invent" shiny terms. Aspiring professionals, act professionally, do your homeworks monoliths well.

Marisamariscal answered 22/4 at 12:3 Comment(2)
Looks like a very angry answer from an old guy, but I don't know your feelings and I don't know your age ;-) Generally, I strongly agree with your theses about monolith, this is about justification for being a programmer. I think, that SOA is awful, because is overengineered by design. Too many sticks and ropes with vendor locks over the top. It's against simplicity. But it's my own opinion - thank you for your answer.Intrigant
@GerardJaryczewski Hey, thanks for feedback :) I wonder what would you say about my age if I had critisized grown ups (as if) that cannot use multiplication table (that is 4K years old) in a daily life. To the point: SOA is architecture - no vendor - not an implementation - no more overengineered than microservices. It is even much simpler (closer to monolith). Simplistically microservices is just a refined, more sophisticated SOA.Marisamariscal

© 2022 - 2024 — McMap. All rights reserved.