Search code examples
c#asp.net-coredesign-patternsarchitecturemodular-monolith

Cross module communication in modular monolith


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?


Solution

  • 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