Why you should use Modular Monoliths with Umbraco

Streamlining development practices with Umbraco never looked so good...

What is modular monolith architecture, anyway?

So, why opt for a modular monolith? It's the best of both worlds. Clear-cut modules let developers focus on tasks without getting tangled in dependencies, deployments, or infrastructure needs. It's a straightforward and maintainable solution.

The term "monolith" may carry negative connotations due to constant comparisons with microservices. But here's the deal – modular monoliths are not a one-time fix. They serve as a robust foundation for easy upgrades, transitioning to microservices, or adopting complex architectures in the future. And guess what? Umbraco's adaptable framework is the perfect canvas for this flexible approach.

Modular monoliths and vertical slices are made for each other: here's why.

Vertical slice architecture transforms software design by focusing on feature-centric segments, complementing modular monoliths. It enhances maintenance by segmenting applications into discrete modules.

In parallel, vertical slice architecture organizes the codebase into self-contained slices, improving modularity and clarifying responsibilities.

The synergy between modular monoliths and vertical slices creates a structured framework for streamlined development, offering scalability and manageability.

Notably, this approach allows flexible integration of custom paradigms like CQRS and modern architectures like MACH.

Additionally, it accommodates legacy system integration, allowing gradual modernization within the structured architecture.

This seamless integration enables a phased migration from legacy to contemporary structures, ensuring compatibility and future-proofing while leveraging modular monoliths and vertical slices' benefits.

Types of folder structures you can achieve with modular monoliths.

Separation by type:

├── MemberModule
│ ├── Services
│ ├── Controllers
│ ├── Data
│ ├── Models
│ └── ... // Other registration-related components

├── ArticlesModule
│ ├── Services
│ ├── Controllers
│ ├── Data
│ ├── Models
│ └── ... // Other article-related components

Or like:

├── Members
│ ├── Application
│ ├── Domain
│ ├── Infrastructure

├── Articles
│ ├── Application
│ ├── Core
│ ├── Infrastructure

Separation by feature (vertical slices):

├── Modules
│ ├── Member Management
│ │ ├──Registration
│ │ │ ├── RegisterUser
│ │ │ └── ...
│ │ ├── Login
│ │ │ ├── LoginUserCommand.cs
│ │ │ ├── LoginUserCommandHandler.cs
│ │ │ ├── LoginUserController.cs
│ │ │ ├── LoginViewModel.cs
│ │ │ ├── AuthenticationService.cs
│ │ │ ├── UserRepository.cs
│ │ │ └── ...
│ │
│ ├── Articles
│ │ ├── Domain
│ │ ├── Infrastructure
│ │ ├── Features
│ │ └── ...

Note: An alternative is segregating modules by generating new projects and linking them in the main application. Establishing dedicated Dependency Injection (DI) module registration within each module is good, encapsulating its own DI mechanism.

How to use modular monoliths with Umbraco.

Let's look at an Umbraco project showcasing seamless modular monolith integration! This article zooms in on two core modules: members and articles, covering user functions and article display.

Note: We're keeping it simple with a single Umbraco project, skipping the composition root and its own DI.

Example 1: Integrating member's modules into modular monolithic architecture.


Project Structure:

Note: The member modules boast diverse architectural structures. In this example, the members' module utilizes vertical slice architecture with CQRS, including a standard module containing technically related shared elements. The Article module adopts standard MVC architecture.

Packages Used:

  • Scrutor: Aids in dependency registration.

  • FluentValidation: Facilitates the creation of fluent validation logic for models.

Member Module Overview:


using FluentValidation;
using ModularMonolith.TheUntoldStory.Common.CQRS;

namespace ModularMonolith.TheUntoldStory.Modules.Members;

public static class MembersModule
{
    public static IUmbracoBuilder AddMembersModule(this IUmbracoBuilder umbracoBuilder)
    {
        _ = umbracoBuilder.Services
            .AddMembers();

        return umbracoBuilder;
    }

    public static IServiceCollection AddMembers(this IServiceCollection serviceCollection)
    {
        // Commands
        _ = serviceCollection
            .Scan(s => s.FromAssemblyOf<IMembersModule>()
                .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>)))
                .AsImplementedInterfaces()
                .WithScopedLifetime());

        // Queries
        _ = serviceCollection
            .Scan(s => s.FromAssemblyOf<IMembersModule>()
                .AddClasses(c => c.AssignableTo(typeof(IQueryHandler<,>)))
                .AsImplementedInterfaces()
                .WithScopedLifetime());

        // Validators
        _ = serviceCollection
            .Scan(s => s.FromAssemblyOf<IMembersModule>()
                .AddClasses(c => c.AssignableTo(typeof(IValidator<>)))
                .AsImplementedInterfaces()
                .WithScopedLifetime());

        return serviceCollection;
    }
}

internal interface IMembersModule
{
}

Simple module registering all required CQRS dependencies: queries, commands, validators.

Example 2: Incorporating articles module into modular monolithic architecture.


using ModularMonolith.TheUntoldStory.Modules.Articles.Mappers;
using ModularMonolith.TheUntoldStory.Modules.Articles.Notifications;
using ModularMonolith.TheUntoldStory.Modules.Articles.Services;
using ModularMonolith.TheUntoldStory.Modules.Articles.Services.Implementations;
using Umbraco.Cms.Core.Notifications;

namespace ModularMonolith.TheUntoldStory.Modules.Articles;

public static class ArticlesModule
{
    public static IUmbracoBuilder AddArticlesModule(this IUmbracoBuilder umbracoBuilder,
        Action<ArticleModuleConfiguration>? moduleOptions = null)
    {
        // Async Notifications
        _ = umbracoBuilder
            .AddNotificationAsyncHandler<ContentPublishedNotification, ContentNotifications>()
            .AddNotificationAsyncHandler<ContentUnpublishedNotification, ContentNotifications>()
            .AddNotificationAsyncHandler<ContentMovedNotification, ContentNotifications>()
            .AddNotificationAsyncHandler<ContentMovedToRecycleBinNotification, ContentNotifications>();

        // Sync Notifications
        _ = umbracoBuilder
            .AddNotificationHandler<ContentSavingNotification, ContentNotifications>();

        // Services
        _ = umbracoBuilder.Services
            .AddArticlesModule(moduleOptions);

        return umbracoBuilder;
    }

    public static IServiceCollection AddArticlesModule(this IServiceCollection serviceCollection,
        Action<ArticleModuleConfiguration>? moduleOptions = null)
    {
        _ = serviceCollection
            .AddControllers();

        // Services
        _ = serviceCollection
            .AddScoped<IArticleService, ArticleService>()
            .AddScoped<IArticleSearchService, ArticleSearchService>();

        // Options
        moduleOptions ??= _ => { };

        _ = serviceCollection
            .Configure<ArticleModuleConfiguration>(moduleOptions);

        // Mappers
        _ = serviceCollection
            .Scan(s => s.FromAssemblyOf<IArticleModule>()
                .AddClasses(c => c.AssignableTo(typeof(IMapper<,>)))
                .AsImplementedInterfaces()
                .WithScopedLifetime());

        return serviceCollection;
    }
}

public sealed class ArticleModuleConfiguration
{
    public int RelatedArticles { get; set; } = 3;
    public string DefaultCategory { get; set; } = string.Empty;
}

internal interface IArticleModule
{
}

Articles Module Configuration: The articles module registers Umbraco notifications and dependencies, allowing external control over configurations based on usage.

Module Benefits:

  1. Centralized access to logic and dependencies.

  2. Internal encapsulation for enhanced module isolation.

  3. Transparency and separation maintenance within the module.

  4. Facilitation of onboarding with an intuitive approach to feature logic.

Note: The desired visibility of internal classes/records in test projects can be configured using the method outlined here.

Module Registration in Umbraco: In the main Umbraco application (Startup.cs), modules are registered, including optional/required configurations.


using ModularMonolith.TheUntoldStory.Modules.Articles;
using ModularMonolith.TheUntoldStory.Modules.Members;

namespace ModularMonolith.TheUntoldStory
{
    public class Startup
    {
        // ...
        // ...
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddUmbraco(_env, _config)
                .AddBackOffice()
                .AddWebsite()
                .AddDeliveryApi()
                .AddComposers()
                .AddMembersModule()
                .AddArticlesModule(articleConfiguration =>
                {
                    articleConfiguration.RelatedArticles = 6;
                    articleConfiguration.DefaultCategory = "News";
                })
                .Build();
        }

        // ...
        // ...
        // ...
    }
}


Note:
Utilizing Umbraco's IComposer auto-registers the module upon addition to the project.

Scaling & Extraction: Effortlessly remove modules for independent use, resembling microservices. External shipping and encapsulation support DevOps practices.

In-Module Communication Post-Extraction: Explore APIs, gRPC, SignalR, or contract interfaces for inter-module communication. Asynchronous communication via a message broker is a viable method.

Exploring Umbraco's Modular Monolith: Underscores robust project structuring. 'Members' and 'Articles' modules showcase adaptability, emphasizing encapsulation and internal visibility.

Should the Modular Monolith Story Stay Untold?

Modular monoliths and vertical slices pave the way for streamlined development and evolution, ensuring agile systems! Why not embrace simplicity, adaptability, and future-proofing? I think this is an important story to be told; what do you think?

A few of my favourite writers on this topic! Check them out:

  • Image for Why you should use Modular Monoliths with Umbraco Build

    Why you should use Modular Monoliths with Umbraco

  • Image for 5 Tips to Keep Your Zoom Call Secure Strategy

    5 Tips to Keep Your Zoom Call Secure

  • Image for Keeping Up With Digital Transformation Build

    Keeping Up With Digital Transformation

  • Image for Website Security Checklist: The 4 Basics Build

    Website Security Checklist: The 4 Basics

Ready to collaborate ?

Get in touch to see how we can transform your digital presence.

Send us a message