Modular monolith

Hi,

I’m trying to build a proof of concept for an enterprise system based on the modular monolith architecture using NServiceBus. Hence, the modules/bounded contexts live in the same process and communicate with each other using events.

When a module publishes an event, it is then handled in another module, or in multiple modules, coexisting in the same process. This works as expected.

I am wondering if this is a usual scenario for NServiceBus and if it’s suitable for production. Are there any particular aspects that we should pay attention to in such a scenario?

Thank you in advance!

Hi @adoloca,

Welcome to the Particular discussion group.

My main concern would be related to “congestion”. When using a monolithic/modular architecture there will be only one endpoint, unless you’re going down the route of multi-hosting, which means a couple of things:

  • one input queue where all messages will back up
  • multiple subscribers won’t work and the subscriber logical endpoint is one, so if more than one module needs to subscribe only one of them will effectively receive the message

Are you planning to use multi-hosting?

Hi Mauro,

In the proof of concept that I have built, the events are published by a Web API application. These events are then handled by a console application (one nsb endpoint) in several modules (separate assemblies). What I could test until now, is that an event published by the Web API is received by all modules that have a handler for it, in the console app. Only when the console app is scaled out, the event is received by just one of the console app instances. This behavior is actually desired for the modular monolith.

So using multi-hosting would be rather justified by the possible congestion risk. Is that right?

By the way, can multi-hosting be implemented also in a .Net Framework application? Is there a code sample available?

Thank you in advance!

Yes, you’re correct. My bad, what I really wanted to say is that when handling events in such a scenario, all handlers will be invoked in the context of the same incoming message. Something like the following:

  • input queue: message
  • endpoint picks up the message
  • endpoint looks for handlers
  • endpoint finds n handlers for the incoming message type
  • endpoint will invoke all found handlers (in a sort of for loop)

If one of the handlers fails the incoming message will be retried and all handlers will be invoked again, which might lead to some issues.

I don’t think there is any. How are you hosting?

I’m hosting in OWIN:

public partial class Startup
{
	public void Configuration(IAppBuilder app)
	{	
		...
		endpointConfiguration.EnableInstallers();
		var transport = endpointConfiguration.UseTransport<RabbitMQTransport>();
		transport.ConnectionString("host=localhost");
		transport.UseConventionalRoutingTopology();
		var endpointInstance = await NServiceBus.Endpoint.Start(endpointConfiguration).ConfigureAwait(false);
		...
	}
}

You can create multiple EndpointConfiguration instances, with different endpoint names, and start all of them one after the other.

The thing you need to be careful with is to scope assembly scanning by endpoint instance; otherwise, all endpoint will load all handlers regardless.

You can use an extension method like the following:

public static AssemblyScannerConfiguration IncludeOnly(this AssemblyScannerConfiguration configuration, params string[] assembliesToInclude)
{
   var excluded = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
                .Select(path => Path.GetFileName(path))
                .Where(existingAssembly => !assembliesToInclude.Contains(existingAssembly))
                .ToArray();

   configuration.ExcludeAssemblies(excluded);

   return configuration;
}

and use it like follows:

var scanner = configurationForModuleA.AssemblyScanner();
scanner.IncludeOnly("MyModuleAHandlers.dll", "MyMessages.dll");