docs/en/Community-Articles/2025-02-20-Using-OutboxInbox-Pattern-for-Reliable-Event-Handling-in-a-Multi-Module-Monolithic-Application/POST.md
This article explains how to implement reliable event handling using the Outbox/Inbox pattern in a modular monolithic application with multiple databases. We'll use the ModularCRM project as an example (how that project was created is explained in this document).
ModularCRM is a monolithic application that integrates multiple ABP framework open-source modules, including:
AccountIdentityTenant ManagementPermission ManagementSetting ManagementBesides the ABP framework modules, the project contains three business modules:
Ordering), using MongoDB databaseProducts), using SQL Server databasePayment), using MongoDB databaseThe project configures separate database connection strings for ModularCRM and the three business modules in appsettings.json:
{
"ConnectionStrings": {
"Default": "Server=localhost,1434;Database=ModularCrm;User Id=sa;Password=1q2w3E***;TrustServerCertificate=true",
"Products": "Server=localhost,1434;Database=ModularCrm_Products;User Id=sa;Password=1q2w3E***;TrustServerCertificate=true",
"Ordering": "mongodb://localhost:27017/ModularCrm_Ordering?replicaSet=rs0",
"Payment": "mongodb://localhost:27017/ModularCrm_Payment?replicaSet=rs0"
}
}
These modules communicate through the ABP framework's DistributedEventBus to implement the following business flow:
This is a simple example flow. Real business flows are more complex. The sample code is for demonstration purposes.
OrderPlacedEto event when an order is placedOrderPlacedEto event and reduce product stockOrderPlacedEto event, processes payment, then publishes PaymentCompletedEto eventPaymentCompletedEto event and updates order status to DeliveredWhen implementing this flow, we need to ensure:
Using the default implementation of the ABP framework's distributed event bus cannot meet these requirements, so we need to add a new mechanism that is also provided by the ABP Framework.
To meet these requirements, we use the Outbox/Inbox pattern:
For how to enable and configure
Outbox/Inboxin projects and modules, see: https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events
Each module needs to configure separate Outbox/Inbox. Since it's a monolithic application, all message processing classes are in the same project, so we need to configure Outbox/Inbox for each module with Selector/EventSelector to ensure that the module only sends and receives the messages it cares about, avoiding message duplication processing.
ModularCRM Main Application Configuration
It will send and receive messages from all ABP framework open-source modules.
// This selector will match all abp built-in modules and the current module.
Func<Type, bool> abpModuleSelector = type => type.Namespace != null && (type.Namespace.StartsWith("Volo.") || type.Assembly == typeof(ModularCrmModule).Assembly);
Configure<AbpDistributedEventBusOptions>(options =>
{
options.Inboxes.Configure("ModularCrm", config =>
{
config.UseDbContext<ModularCrmDbContext>();
config.EventSelector = abpModuleSelector;
config.HandlerSelector = abpModuleSelector;
});
options.Outboxes.Configure("ModularCrm", config =>
{
config.UseDbContext<ModularCrmDbContext>();
config.Selector = abpModuleSelector;
});
});
Order Module Configuration
It only sends OrderPlacedEto events and receives PaymentCompletedEto events and executes OrderPaymentCompletedEventHandler.
Configure<AbpDistributedEventBusOptions>(options =>
{
options.Inboxes.Configure(OrderingDbProperties.ConnectionStringName, config =>
{
config.UseMongoDbContext<IOrderingDbContext>();
config.EventSelector = type => type == typeof(PaymentCompletedEto);
config.HandlerSelector = type => type == typeof(OrderPaymentCompletedEventHandler);
});
options.Outboxes.Configure(OrderingDbProperties.ConnectionStringName, config =>
{
config.UseMongoDbContext<IOrderingDbContext>();
config.Selector = type => type == typeof(OrderPlacedEto);
});
});
Here, the
EventSelectorandHandlerSelectorchecks only a single type. If you have multiple events and event handlers, you can check the given type if it is included in an array of types.
Product Module Configuration
It only receives EntityCreatedEto<UserEto> and OrderPlacedEto events and executes ProductsOrderPlacedEventHandler and ProductsUserCreatedEventHandler. It does not send any events now.
Configure<AbpDistributedEventBusOptions>(options =>
{
options.Inboxes.Configure(ProductsDbProperties.ConnectionStringName, config =>
{
config.UseDbContext<IProductsDbContext>();
config.EventSelector = type => type == typeof(EntityCreatedEto<UserEto>) || type == typeof(OrderPlacedEto);
config.HandlerSelector = type => type == typeof(ProductsOrderPlacedEventHandler) || type == typeof(ProductsUserCreatedEventHandler);
});
// Outboxes are not used in this module
options.Outboxes.Configure(ProductsDbProperties.ConnectionStringName, config =>
{
config.UseDbContext<IProductsDbContext>();
config.Selector = type => false;
});
});
Payment Module Configuration
It only sends PaymentCompletedEto events and receives OrderPlacedEto events and executes PaymentOrderPlacedEventHandler.
Configure<AbpDistributedEventBusOptions>(options =>
{
options.Inboxes.Configure(PaymentDbProperties.ConnectionStringName, config =>
{
config.UseMongoDbContext<IPaymentMongoDbContext>();
config.EventSelector = type => type == typeof(OrderPlacedEto);
config.HandlerSelector = type => type == typeof(PaymentOrderPlacedEventHandler);
});
options.Outboxes.Configure(PaymentDbProperties.ConnectionStringName, config =>
{
config.UseMongoDbContext<IPaymentMongoDbContext>();
config.Selector = type => type == typeof(PaymentCompletedEto);
});
});
ModularCrm directory:# Start SQL Server and MongoDB databases in Docker
docker-compose up -d
# Restore and install project npm dependencies
abp install-lib
# Migrate databases
dotnet run --project ModularCrm --migrate-database
# Start the application
dotnet run --project ModularCrm
https://localhost:44303/ to view the application homepageApplication logs display the complete processing flow:
[Ordering Module] Order created: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9, CustomerName: john
[Products Module] OrderPlacedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, CustomerName: john, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9
[Products Module] Stock count decreased for ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9
[Payment Module] OrderPlacedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, CustomerName: john, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9
[Payment Module] Payment processing completed for OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88
[Ordering Module] PaymentCompletedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, PaymentId: d0a41ead-ee0f-714c-e254-3a1834504d65, PaymentMethod: CreditCard, PaymentAmount: ModularCrm.Payment.Payment.PaymentCompletedEto
[Ordering Module] Order state updated to Delivered for OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88
In addition, when a new user registers, the product module will also receive the EntityCreatedEto<UserEto> event, and we will send an email to the new user, just to demonstrate the Outbox/Inbox Selector mechanism.
[Products Module] UserCreated event received: UserId: "9a1f2bd0-5b28-210a-9e56-3a18344d310a", UserName: admin
[Products Module] Sending a popular products email to [email protected]...
By introducing the Outbox/Inbox pattern, we have achieved:
ModularCRM project not only implements reliable message processing but also demonstrates how to handle multi-database scenarios gracefully in a monolithic application. Project source code: https://github.com/abpframework/abp-samples/tree/master/ModularCrm-OutboxInbox-Pattern