Modular Monolith Presentation by Milan Jovanović
Source: YouTube
Overview
Introduction to Modular Monoliths and Lessons Learned.
These are key notes I'm taking from this video.
The Monolith
- One application/solution represents the whole system.
- Considered old-fashioned
- Limited Scalability - it all scales together.

Microservices
- Many individual "services" form the system. Each service does one job.
- Considered cutting edge and cool.
- Infinite scalability (in theory) - you just scale the service independently.

Comparison
| Monolith | Microservice |
|---|---|
| One deployment artifact | Many deployment artifacts |
| Communicate via method calls | Communicate via network calls |
| Vertically scalable - add more server grunt | Horizontally and vertically scalable |
| One Database | Many databases - usually one per service |
| Transactions | Eventual consistency |
| Difficult with large teams (conflicts) | Scales well with large teams |
You shouldn't start a new project with microservices, even if you're sure your application will be big enough to make it worthwhile
- Martin Fowler
If you start with Microservices you risk making the service too granular. if you start with a monolith you will naturally see where the bottlenecks are in the system and break it down accordingly.
But a monolith is usually difficult to refactor into microservices. What we ideally need is:
- The physical architecture of a monolith.
- The logical architecture of microservices. (individual features naturally grouped together).
- Ability to move to a microservice architecture easily
This is where the Modular Monolith comes in.
The Modular Monolith
A modular monolith is a software design approach in which a monolith is designed with an emphasis on interchangeable (and potentially reusable) modules.
A modular monolith is an explicit name for a monolithic system designed in a modular way.
Modular: Consisting of separate parts that, when combined, form a complete whole, or made from a set of separate parts that can be joined together to form a larger object.

Each of the "services" is a module inside the monolith. All of the mdules interact with the same database. There are explicit boundaries around the modules.
Challenges
Modular monoliths come with several challenges.
- Defining Modules and bounded contexts.
- Communication between modules.
- Module data independence and isolation.
Defining Modules
- Modules should represent cohesive sets of functionalities. Things that naturally fit together.
- Bounded context.
- The boundary within a domain where a particular domain model applies.
- A single table in the database can belong to more than one bounded context, for example a user in the context of orders, payments and shipments.
- Each module can be treated as a seperate application.
Module Architecture
It doesn't matter which of the following architectures you choose for your modules, you can even choose a different architecture for each module, one module should be architected like an individual application.
Layered Architecture

Clean Architecture

Together multiple layers make up a module.
Vertical Slice Architecture

Example
A good example of an application created as a Modular Monolith with DDD can be found at https://github.com/kgrzybek/modular-monolith-with-ddd
This is good to understand the concept then expand with your own requirements.
Communication Between Modules
Each module should expose a public API that other modules can call.
graph LR
a[Module A]
b[Module B]
a -- Public API --> b
This could be an interface that other modules call.
It is important that there are no references between modules except via a public API. Implementation details must be hidden, for example via the internal keyword in C#.
There are two approaches to implementing this:
Module Communication - Method Calls
Fast, in-memory calls.
graph LR
subgraph "Orders Module"
o[Orders Component]
end
subgraph "Catalog Module"
c[Catalog Component]
end
o -- Method Call --> c
In this scenario the Catalog Module has a public API, the Order Module needs to call that API. The Order module can reference an interface to the API, which is given the actual implementation ar runtime via dependency injection.
Remember, you can only call the public API of other modules.
The problem here is that this introduces runtime coupling. This causes issues if you extract this to a microservice later. You would need to reimplement this with something like HTTP calls in this situation.
Module Communication - Messaging
This is asynchronous. If a module wants to interact with another it sends messages over a message bus. It is possible to implement RPC-like calls ove a message bus.
Libraries like MassTransit implement a send request/response message model.
You need a correlation ID between messages (mass transit abstracts this away).
To use this kind of communication you define interfaces for message contracts. You can share these via a shared library where all contracts for a shared module reside, or all the application to manage in one place.
This approach of using a message bus is completely decoupled. This is still supported if we break this into microservices. It is easier to extract modules.
However too much messaging can result in performance issues.
Module Data Independence
Every module should be responsible for its own data.
This constraint must be imposed, as sharing of data between modules results in a mess.
Querying data directly from different modules in the system is not allowed.
We need to introduce different levels of data isolation between modules.
How do we isolate data between modules
There are 4 levels.
If all the data is in the same database this results in no isolation. This is the worst approach and should be avoided. Foreign keys between tables introduces coupling, etc.
The next level up is to use the same database, with different schemas. Now we have logical separation of the tables, each module should have its own schema. You must also not impose foreign keys and this will hinder future migration. There must be absolutely no querying between schemas.
We could use different physical databases (which could be on the same database server).
Finally we could have different databases with different DBMS types suitable for the data being held, for example SQL Server and CosmosDb.
Which to choose depends on the requirements, but a good start is either different schemas, or different databases (on the same host).
Some things to look out for
- Spend time defining module boundaries - it will pay dividends later. You need to think about future system growth.
- Eventual consistency is great, but you need to plan for it. if the modules are independent and communication over a message bus, this can turn out to be an issue. Some changes may be slow to render on the user interface. The UI can do things like eager updates to behave as if the update is completed in the database, and react finally to the response. Web sockets can help. Distributed transactions are complicated and slow, two phase commits aren't great.
- Consider merging chatty modules. Too many modules communicating constantly implies the module boundaries are incorrect - and too granular.
- Carefully plan how data will be shared between modules. Chatty modules may imply data is missing from one module. You want you modules to be publishing messages when an important change occurs, that other modules can subscribe to for a local copy or cache.