How Modular Monolithic Architecture Handles Communications Between Modules — In-process Method Calls (Public APIs)
We’re going to dive into the Modular Monoliths Architecture and explore how different modules communicate with each other within a monolith.
We’ll specifically focus on synchronous communication using in-process method calls, also known as public APIs. Let’s get started! 🚀
Why is Module Communication Important?
In a Modular Monoliths Architecture, different modules often need to interact to fulfill business requirements. Effective communication between these modules is crucial for the application to function seamlessly. Let’s discuss why this is important:
- Business Logic Execution: Many business processes require data or services from multiple modules. For example, adding an item to a basket might require fetching product details from a catalog module.
- Data Consistency: Ensuring data consistency across modules is vital for maintaining the integrity of the application.
- Scalability and Performance: Efficient communication methods can significantly enhance the performance and scalability of the application.
- Modularity: Keeping modules well-defined and allowing them to communicate effectively promotes modularity and maintainability.
Synchronous Communication Overview
Synchronous communication involves direct method calls between modules. When one module calls a method in another module, it waits (or blocks) until the called method completes its execution and returns a response. This method of communication is straightforward and easy to implement within a single process. Let’s break down how this works:
- Direct Calls: The calling module directly invokes a method exposed by another module.
- Blocking Operations: The calling module waits for the response, ensuring that operations are executed in a sequence.
- Immediate Feedback: This method provides immediate feedback, making it ideal for operations that require real-time processing.
In-process Method Calls (Public APIs)
One of the most common ways to achieve synchronous communication in a modular monolithic architecture is through in-process method calls. Let’s see how this works:
What Are In-process Method Calls?
In-process method calls are direct calls to methods exposed by other modules within the same application process. These methods act as public APIs that other modules can use.
- Public APIs: Each module exposes a set of methods that can be called by other modules.
- Interfaces: These APIs are typically defined in interfaces, ensuring a clear contract for interaction and promoting loose coupling.
- Direct Execution: The call is executed directly, without any network overhead, resulting in high performance.
Here’s an example of how you might define an interface for a catalog module that other modules can call:
public interface ICatalogService
{
Product GetProductById(Guid productId);
bool UpdateProductPrice(Guid productId, decimal newPrice);
}
Benefits of In-process Method Calls
Using in-process method calls for module communication offers several advantages:
Performance:
- Communication occurs within the same process, minimizing overhead.
- No network latency, unlike inter-process or remote communications.
Simplicity:
- Easy to implement and understand.
- Leverages the language’s native method call mechanisms, which developers are already familiar with.
Ease of Debugging:
- Debugging in-process calls is straightforward with standard debugging tools.
- No need for complex setups or external monitoring tools.
Strong Typing:
- Compile-time checking ensures that method signatures and data types are consistent across modules.
- Reduces the risk of runtime errors due to type mismatches.
Handling Dependencies and Decoupling
To maintain modularity and decoupling in a modular monolithic architecture, it’s crucial to handle dependencies effectively. Here’s how you can do it:
Dependency Injection (DI):
- Use DI to manage dependencies between modules.
- The DI container manages the lifecycle and resolution of dependencies, making the architecture flexible and testable.
Interfaces as Contracts:
- Interfaces should be used to define contracts between modules, promoting loose coupling.
- This approach ensures that changes in one module do not directly impact others, as long as the contract (interface) remains unchanged.
Shared Contract Libraries:
- Create shared contract libraries that contain common interfaces used for inter-module communication.
- Modules reference these shared libraries and perform in-process method calls using patterns like MediatR for synchronous operations.
Example of Dependency Injection Setup
Here’s a basic example of how you might configure DI for a catalog module:
public static class CatalogModule
{
public static IServiceCollection AddCatalogModule(this IServiceCollection services)
{
services.AddTransient<ICatalogService, CatalogService>();
return services;
}
}
In your Program.cs
, you can then add:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCatalogModule();
var app = builder.Build();
app.Run();
This setup ensures that all dependencies are correctly injected, promoting a clean, modular architecture.
Conclusion
Synchronous communication using in-process method calls is a powerful and efficient way to handle interactions between modules in a modular monolithic architecture.
By exposing public APIs and leveraging dependency injection, you can create a robust and maintainable application. While there are challenges, such as managing dependencies and maintaining decoupling, careful design and adherence to best practices can mitigate these issues.
This is step-by-step development of reference Modular Monoltihs Architecture on .NET used ASP.NET Web API, Docker, PostgreSQL, Redis, RabbitMQ, Keycloak, Seq, MassTransit, Entity Framework Core, CQRS, MediatR, DDD, Vertical Slice Architecture and Outbox Pattern implementation with using latest features of .NET 8 and C# 12.