galbenu.ro

Modular monolith

Nov
23

Monoliths are OK. There is nothing wrong with a good designed monolith. But what qualifies as a good monolith? In my opinion,  it means modular = composed of modules.

A module is a subdivision that is independent of the other subdivisions. It has all the data needed to respond to an external request. This implies that, when a client sends a command or a query to this module, it may not call other modules to get the data. It may use only data that it owns.

If a module needs data from other modules, which is often the case, it may get that data only outside the client’s request, i.e. in the background. By client I mean any requester that lives outside this module, i.e. a human, an API or other module.

The cleanest way to get external data is by using domain/integration events or by subscribing to queries, when it is possible. There are other ways, but by using events is by far my preferred way.

You know that you have a modular monolith when you delete a module and the application still works, of course, without the functionality provided by the deleted module. The widgets, the buttons, the components or the pages owned by that module are just missing. As a concrete example, when you delete the Statistics module, all the stats counters and the stats charts will be missing but the containing pages will continue to work, without any fatal error. The page should also load faster.

Developing a modular monolith depends on the programming language. I’m mainly a PHP developer, with a C++ background (and a former Captain of the armed forces 🙂 ). I think PHP is great for backend web application so my examples refer to PHP. I strongly encourage you read this even though you are not using PHP.

Next I will present the rules that I follow when developing a modular monolith.

Rule 1: Each modules must have it’s own dependency injection container

This is first step to ensure that every module is independent. The Application that combine the modules has also a dependency injection container (DIC), named the default container, and it delegates the resolving of dependencies down to each module’s DIC. The default DIC detects cross-module dependencies. In order to respect the DRY principle, it also resolves common/shared services.

Each module’s DIC is registered to the default DIC by specifying it’s instance and the namespaces that it handles.

Example of a modular monolith

In the image above you can see the src/ directory, where all the modules are placed, along with the app/ (the application) and shared/ (libraries).

The expanded module is Catalog, where the products are described, with texts, images, characteristics and so on. 

  1. the config/ directory contains the DIC and the routes. 
  2. the src/ contains the source code; it is organized in layers;
  3. the src/Catalog is the domain layer, that has no dependency to any other layers;
  4. the other directories are self describing.

Rule 2: A module may not depend on other modules

There should be no service injected from other modules; there must be a way to detect cross-module dependencies; only Events, Commands and Queries may be used;

Rule 3: Each UI Component must provide its own resources

Each UI Component (just Component from now on) must provide its own CSS or Images. A Component must be organized in a directory that contains all the code and it’s resources;

Example of a modular component

You can see in the above picture how a component is organized. The component is called EditareProdusComponent. It has a PHP class, EditareProdusComponent.php and a directory where all the resources are defined. A SCSS file, a JS file and a PHP renderer.

The JS and SCSS files are referenced from the renderer file, like this:

$plugins->headCss->add(__DIR__ . '/editare-produs.scss');
echo $plugins->js->requireModule(__DIR__ . '/editare-produs.js');

$plugins is an Application component that gathers all the resources, translates their URLs and references them in the body of the rendered page, i.e. in the <head> or at the end of the <body> tag.

Each Component should use its own ReadModel, specially designed to respond as fast as possible to queries. If this is not feasible, then each sub-module should use its own ReadModel. For example, all components from the Product editing sub-module could share the same EditingProductDetailsReadModel.

Rule 4: Each modules should declare its own dependencies

In PHP, each module should have a composer.json file and should be installed with composer.

The Application will reference the individual modules in its composer.json file.

The ModularContainer

In order for the Application to resolve services in the modules, it uses a special DIC: the ModularContainer. This delegates the resolving to the individual containers and ensures that a module will not call another module. It uses the Composition pattern. The initialization of the container is done as it follows:

$container = new ModularContainer();

$container->setDefaultModuleNamespaces(["Gica", "Ui"]);

$appContainer = (require __DIR__ . '/../src/app/config/composition-root.php')($container);
$container->setDefaultContainer($appContainer);
$container->addModuleContainer([], $appContainer);
$container->addModuleContainer(['Drei\\Catalog', 'Drei\\CatalogUi', 'Drei\\CatalogInfrastructure', 'Drei\\CatalogProcesses', 'Import'], (require __DIR__ . '/../src/catalog/config/composition-root.php')($container));
$container->addModuleContainer(['Drei\\Forum'], (require __DIR__ . '/../src/forum/config/composition-root.php')($container));
$container->addModuleContainer(['Drei\\Identity', 'Drei\\IdentityInfrastructure', 'Drei\\IdentityUi',], (require __DIR__ . '/../src/identity/config/composition-root.php')($container));
$container->addModuleContainer(['Drei\\Developer', ], (require __DIR__ . '/../src/developer/config/composition-root.php')($container));
$container->addModuleContainer(['Drei\\InscriereOnline', 'Drei\\InscriereOnlineUi', 'Drei\\InscriereOnlineAdmin', ], (require __DIR__ . '/../src/inscriere-online/config/composition-root.php')($container));
$container->addModuleContainer(['Drei\\Statistici', 'Drei\\StatisticiInfrastructure', ], (require __DIR__ . '/../src/statistici/config/composition-root.php')($container));

$container->setService(ContainerInterface::class, $container);
$container->setService(ModularContainer::class, $container);

The Application

Composing modules is done in the Application, which is just a collection of Web Pages/Actions referenced by routes. Each Web page uses Components from different modules to generate the HTML/JSON fragments, assembles them using Layouts and return them to the user/browser. This has many advantages:

  • Single responsibility principle: the Application and the individual modules have only one reason to change; the Application changes only when the Components are added/removed/rearranged, in other words, only when the layout changes.
  • Separation of concerns: each Component knows/cares only about its business and the Application only cares about assembling the pieces.
  • Possibility to load the individual Components in parallel, speeding the page load time.

Conclusion

Creating a modular monolith is hard. There are many obstacles that must be overtaken but it is doable. One should not just create microservices from the beginning. I think this step, the modular monolith, is a required step before entering the microservices world.