1 of 32

MODULAR

MONOLITH ARCHITECTURE

2 of 32

What we’ll cover

    • Sofware Architectures & Comparasions
    • Modular Monoliths
    • Module Implementations in C#
    • Module Communication Patterns

3 of 32

Software Architectures

Standard Monoliths

Microservices

Modular Monoliths

1

2

3

4 of 32

Standard Monoliths

Monolithic architecture is a traditional software development style where all components of an application are tightly integrated and share a single codebase. In a monolithic architecture, the application is built as a single, unified unit, which typically includes the user interface, business logic, and data access layers all bundled together.

5 of 32

Microservices

Microservices architecture is a design approach where an application is built as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific piece of the application's functionality and communicates with other services through well-defined APIs.

6 of 32

A modular monolith is a software architecture that combines the benefits of both monolithic and modular design principles. It retains the simplicity of a monolithic application while organizing the codebase into distinct, loosely coupled modules. Each module represents a specific domain or feature and encapsulates its functionality.

Modular Monoliths

7 of 32

Standard Monolith

Micro services

Modular Monolith

Modularity

Cost

Scalability

Simplicity

Software Architecture Trade-offs

8 of 32

Everything is a trade-off, there is no solution that fits for all.

The First Law of Software Architecture

9 of 32

Module Definition Rules

Module Communication in C#

1

2

4

Modular Monoliths

Host Application

Module Implementation in C#

3

10 of 32

    • Modularity
        • Modularity emphasizes dividing the system into discrete, self-contained modules, each encapsulating a specific piece of functionality.
    • Single responsibility
        • Each module should focus on a single responsibility or domain, ensuring that it handles only one aspect of the application's functionality.

Module Definition Rules

    • Independency from other modules
        • Modules should minimize dependencies on other modules, allowing them to operate and evolve independently.
    • Data Ownership
        • Each module should have ownership of its own data, controlling how data is accessed and manipulated within both application and database layer.

11 of 32

    • The users module provides modularity by being able to run as a single service. (Modularity)
    • The users module has single responsibility because it only operates with user based logic. (Single responisbility)
    • The users module does not have any dependency to the other modules. (Independency)
    • The other modules can only access the users data through the public API’s. (Data Ownership)

Users Module

12 of 32

The host application in a modular monolith is the central application that loads, configures, and runs all the modules. It is the entry point of the application, responsible for initializing the application environment, managing dependencies, and handling the lifecycle of modules.

Host Application

13 of 32

Mediator Pattern

Module Endpoints Registration

Domain & Integration Events

Importing Modules to Host

Public API’s

1

4

2

5

3

Module Implementations in C#

14 of 32

Mediator Pattern

The Mediator pattern manages communication between objects through a central mediator, preventing direct interactions. This reduces dependencies and makes inter-module communication more organized. In C#, MediatR library implements this pattern to handle commands, queries, and events. MediatR is a great choice in modular monolith applications because it simplifies inter-module communication through a single MediatR object and serves as an in-memory event bus, enabling flexible and decoupled communication between modules.

15 of 32

Public API’s

In modular monolith applications, modules communicate with each other through public APIs. Although modules cannot directly reference one another, a separate project with types. These types can be freely utilized by other modules to perform tasks like sending queries, firing events, or listening for specific events. This approach maintains clear boundaries between modules while allowing for flexible and efficient inter-module communication.

16 of 32

In modular monolith applications, domain events are handled within the module itself, so they don’t need to be shared via the public API. However, the types for integration events should be included in the public API. We use MediatR as an in-memory event bus to manage these events efficiently.

Domain & Integration Events

17 of 32

We have Mediator pattern for handling module communications in various ways. In the Host application, we would need to register all of the endpoints of an module. This requires some helper tools or our own implementation.

Module Endpoints Registration

FastEndpoints

Carter

1

2

18 of 32

FastEndpoints is a lightweight .NET library that simplifies the creation of minimal APIs. It allows developers to define endpoints using a class-based approach, where each endpoint is represented by a class inheriting from Endpoint<TRequest, TResponse>. This framework automatically discovers and registers these endpoints, handling routing, request/response handling, validation, and more, making API development fast and straightforward.

FastEndpoints

19 of 32

Carter is a library designed for ASP.NET Core applications that simplifies and modularizes the creation of API endpoints. It provides an easy and structured approach for defining minimal APIs, making your application more manageable and organized. API endpoints can also have model binding, response customization, validation support. You can also use carter middlewares for your use cases. Carter is based on modularity.

Carter

20 of 32

Each module should have it’s own method to register its services.

Importing Modules to Host

21 of 32

Direct Synchronous Calls

The Mediator Pattern

The Outbox Pattern

Message Brokers

Materialized Views

1

2

4

3

5

Module Communication Patterns

22 of 32

Direct synchronous calls can be in many ways.

    • Loading the assembly and required methods
    • HTTP Calls
    • Using public services through modules

Direct Synchronous Calls

23 of 32

The Mediator Pattern

The MediatR library allows us to communicate between modules without any other dependency or injection. In the example below, the AddItemToCartHandler is trying to get book details from the Books module. Since we have an public contract object and a mediator, everything is fine.

24 of 32

Message brokers can be used in order to communicate with modules. All of the modules can handle events, commands and queries based on the requirements.

We can use RabbitMQ with MassTransit library to execute a query to Users module from Order Processing module.

Message Brokers

25 of 32

26 of 32

Materialized views are effective for optimizing data retrieval in a modular monolith, but challenges arise when a module evolves into a microservice with its own database. Relying on database-level materialized views can create complications if data storage changes. To solve this, the application should handle data retrieval, making the storage type or location irrelevant. Redis is an excellent choice for this, allowing efficient caching and access to precomputed data, ensuring the system remains flexible and performant as the architecture evolves.

Materialized Views

27 of 32

Materialized Views

28 of 32

Architecture Tests

Module to Microservice

1

2

Bonus Content

29 of 32

Architecture Tests

Architecture tests in a modular monolith are valuable for ensuring that modules remain independent and adhere to established design principles. Using the ArchUnit.Net library, you can enforce rules to verify that no modules depend on each other and that the overall architecture aligns with your defined standards.

30 of 32

Module to Microservice

A modular monolith combines the strengths of both monolithic and microservice architectures, balancing simplicity with modularity. However, when the time comes to separate a specific module and turn it into an independent microservice, several crucial steps must be taken:

    • Remove the Contracts Project: The contracts project, which enables in-process communication between modules, becomes redundant once a module is extracted as a microservice. It should be removed to reflect the new architecture.

    • Redesign the Communication Layer: After removing the contracts project, the codebase will naturally show errors. As the module transitions to a microservice, all communication mechanisms must be re-evaluated and rewritten to suit the new distributed environment.

31 of 32

Resources for Modular Monoliths

32 of 32

Teşekkürler!