Firstly, let's talk about Traditional Monolith approach. This approach focuses on layers. It includes three layers, UI, Business and Data. All features in a project are vertically separated into these layers. Among those three layers, the business layer is the one that contains business logics of all features. Each feature knows business logic of other features, which is a fact we call tightly coupled.
The main focus of Modular Monolith is to separate modules. Each module has its own layers ( Domain, Infrastructure and API etc.). Thus, they can use different database solutions. On the other hand, modules don't share their own business logics with each other. They can communicate with each other with sync or async approaches. These approaches are called loosely coupled.
Modular Monolith has a single binary. Therefore, apps that implement Modular Monolith can't be scaled horizontally. Also modules are separated logically, not physically. So Modular Monolith doesn't have a physically distributed structure. If we want to physically separate it, we have to implement the microservice approach.
Traditional Monolith isn't a bad choice but it is likely to turn into a big ball of mud due to its structure. Its transition to microservice is also very difficult. On the other hand, Modular Monolith can easily migrate to microservice. Microservice is a good choice if your application will be published at internet scale. But Modular Monolith is a better option if your application will be published just within an organization.
Communication in Modular Monolith
Let's consider an ordering system in an e-commerce app. Basic modules in an ordering system are Inventory, Order and Payment. During a buying process, order is received, stock is checked and payment is made. If a product is out of stock or payment hasn't been made, that order is cancelled. Then how does such a communication occur in Modular Monolith?
There are two approaches (sync, async) for communication. In the sync approach, each module exposes an interface. These interfaces are defined in the shared layers of modules. The inventory module exposes IInventoryService which has the methods of ReserveStock, ReleaseStock. Likewise, the payment module exposes IPaymentService which has the MakePayment method. The order module communicates with other modules by using these interfaces.
In the async approach, each module raises events. These events are defined in the shared layers of modules. The order module raises the OrderReceived event. The inventory module raises the events of ReserveStock, OutOfStock and ReleaseStock. Likewise, the payment module raises the events of MakePayment, PaymentMade and PaymentFailed. Each module subscribes to related events. So the communication occurs through the message broker.
There are two approaches (choreography, orchestration) in async communication. It doesn't matter which one is preferred. I preferred the orchestration approach because it is more efficient than the choreography approach for this scenario.
How is the project structure?
Bootstrapper is the entry point of the project. So it is a WebApp.
Each module has its own layers which aren't standard. You can apply Onion or Hexagonal Architecture.
Shared.Abstractions : It is used by Modules.Core and Modules.Infrastructure. It contains interfaces, abstractions such as DDD, CQRS etc.
Shared : It is used for cross-cutting concern. It also includes implementations of the abstractions.
Ecommerce.Inventory : It is a class library. It contains the controllers of the inventory module. If you intend to use async method for communication, you can add the consumers (ReserveStockConsumer, ReleaseStockConsumer etc.) in Ecommerce.Inventory.
Ecommerce.Inventory.Core : It contains the domain or business logic of the inventory module. If you wish, you can separate Ecommerce.Inventory.Core into two parts, Application and Domain.
Ecommerce.Inventory.Infrastructure : It is where the modules are connected to external services or components such as database, message queue etc.
Ecommerce.Inventory.Shared : It is used to expose some classes (OutOfStock, StockReserved etc.) and allow the other layers to use those classes.
Using Modular Monolith is a better choice if you don't want your project become a big ball of mud, and your project doesn't require internet scale, or if you are likely to convert your project to a microservice architecture later.
You can access the sample projects from my Github address.