Umbraco Modular Monolith. The Untold Story.

tagged with .NET 6 .NET 7 .NET Core Backend Community Configuration Developer Fun Segmentation Sustainability v10 v11 v12 v13 v14 v9

Ever heard of Umbraco modular monoliths? Nope, it's not some fancy tech jargon. It's about simplifying things in a world where everyone's chasing after complex solutions.

See, initial app architecture discussions often dive eagerly into giant tangled webs of microservices, complex project structures, unstructured layers, logics or monoliths without a thought for modular monoliths.

Modular what? Who??


Surprisingly, not many folks talk about it, but this untold story may change your approach to building stuff with Umbraco.

Understanding Modular Monolith Architecture

In the world of building digital castles, we've got three main blueprints: the grand old monolith, the all-the-rage microservices, and the lesser-known hero, the modular monolith. So, what's the fuss about?

You've got the old-school monoliths, the huge all-in-one structures; just picture an entire city of buildings connected by corridors. Who knows who owns what, where an apartment ends or another begins?

Then there are microservices; that’s like having a bunch of small, specialised neighbourhoods working together, but sometimes they're too spread out and hard to manage.

Now, let's zoom in on the underdog: modular monoliths. The neatly organised districts within a city, where each part has its purpose and space to grow without turning into chaos. It's the sweet spot between simplicity and scalability.

Why go for a modular monolith, you ask? Well, it's like having the best of both worlds. With clear-cut modules, developers can focus on specific tasks without getting tangled up in a web of dependencies, deployments or infrastructure needs. Plus, it's easier to understand and maintain.

The word “monolith” might convey negative connotations for some people due to its constant comparison to well-loved microservices. So understandably, this topic might seem controversial at first glance, but it’s not.

Here's the thing about modular monoliths: they're not just a one-time solution. They can be the sturdy foundation that allows for easy upgrades, shifting to microservices or adopting more complex architectures in the future. And guess what? With its adaptable framework, Umbraco is the perfect canvas for this flexible approach.

Imagine starting your project with a robust base that satisfies your current requirements and ensures a seamless transition whenever you decide to make changes. It's like future-proofing your digital creation right from the get-go, giving it the flexibility to evolve and grow with your project or customer needs.

For more benefits of monolithic architecture and reasons to build them, head to my recent Cogworks blog, Understanding Architecture: Reasons to Build a Modular Monolith First.

Modular Monolith and Vertical Slices. The Gateway to Simplicity.

Vertical slice architecture represents a paradigm shift in software design, emphasising the structuring of applications into cohesive, feature-centric segments rather than traditional layered architectures. This approach aligns seamlessly with modular monoliths due to their shared objectives in rectifying the complexities often associated with monolithic systems.

Within a modular monolith, the application undergoes segmentation into discrete modules, each catering to specific domains or functionalities. This division fosters superior separation of concerns, facilitating enhanced maintenance and extensibility.

Parallelly, vertical slice architecture advocates for organising the codebase into self-contained slices, encapsulating comprehensive sets of functionalities about particular features or use cases. This methodology promotes improved modularity and a more lucid demarcation of responsibilities within the codebase.

The glue that binds modular monoliths and vertical slices is their mutual pursuit for refined code organisation, streamlined maintenance, and extensibility enhancement within monolithic constructs. By connecting these approaches, developers benefit from a structured framework that simplifies the development process and offers scalability and manageability in a monolithic ecosystem. It's the perfect duo if you ask me.

Additionally, it's important to note that while vertical slices and modular monoliths provide a structured framework, they don't restrict the integration of custom architectural paradigms. Developers can introduce specialised approaches like CQRS (Command Query Responsibility Segregation) or adopt other modern architectures like MACH (Microservices, API-first, Cloud-native, Headless) within these modules or slices. This flexibility empowers teams to align their architecture with specific business needs or industry standards, leveraging innovative patterns to enhance system efficiency and scalability.

Furthermore, the adaptability of this approach extends to legacy system integration within modules or slices. By encapsulating legacy components, development teams can gradually modernise their applications within the structured architecture. You can read more about ways to modernise legacy systems in the Cogworks blog.

This seamless integration allows for a phased migration from legacy architectures to contemporary structures, ensuring compatibility and future-proofing while leveraging the benefits of modular monoliths and vertical slices.

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

Please note that an alternative approach involves the segregation of modules by generating new projects and linking them within the main application.

An advisable practice involves establishing a dedicated Dependency Injection (DI) module registration within each module. Ideally, each module should encapsulate its own DI mechanism. For further insights, check out the article Understanding the Composition Root.

Modular Monoliths in Action with Umbraco.

Let us dive into implementing a demo Umbraco project showcasing the formidable integration of a modular monolith architecture!

In this article, let’s focus on two core modules: members and articles!

The members module encapsulates fundamental user functionalities, encompassing registration, login, forgotten password retrieval, and account and password confirmation mechanisms.

The articles module orchestrates the display of individual articles and article listing pages!

Note

For simplicity, we will use a single Umbraco project to cover  the above modules, as we’ll not be using the composition root with its own DI.

Example 1: Integrating members modules into modular monolithic architecture

Modular Monolith project structure

Project structure

It is worth noting that member modules include a variety of architectural structures within their framework.

For this example, the members' module uses the vertical slice architecture combined with CQRS (as you can see, there is a common module that contains shared elements that are mostly technically related). The Article module uses standard (simple) MVC architecture.

Before we dive into the details of what, there were used few packages:

  • Scrutor - it helps with dependency registration
  • FluentValidation - it helps with creating fluent validation logic for models.

Let's have a closer view of a member module


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: Integrating the 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
{
}

Here, the articles module registers Umbraco notifications and all other dependencies (services, mappers, etc.), including some module configurations that can be externally controlled based on where we intend to employ this module.

Some benefits of using the modules:

  • Centralised place for accessing module logic and dependencies.

  • Internal encapsulation: All services, internal objects, etc., remain within the module, reducing visibility to external modules. This encapsulation ensures interactions occur in a designed and controlled manner, enhancing module isolation and coherence.

  • Maintenance of transparency and separation: Module models, controllers, views, etc., are contained within the module itself, preserving clarity in distinguishing between the base configuration of the Umbraco application and our modules.

  • Facilitation of onboarding: New developers can swiftly navigate and comprehend the project's structure and architecture. A comparison between standard MVC within the articles module and vertical slice within the members' module reveals that vertical slicing offers a more intuitive approach to locating dedicated parts of a feature's logic.

Note

While internal classes/records' visibility in test projects might be desired, it's possible to configure this using the method outlined here: Microsoft documentation on 'InternalsVisibleTo'

Now, let's explore how the modules are registered in Umbraco.

In our main Umbraco application (Startup.cs), we perform module registration, including optional/required module configurations if applicable:


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

By utilising Umbraco's IComposer, the module can be automatically registered upon its addition to the project.

Suppose our application necessitates scaling or the extraction of specific modules from the project. In that case, a seamless process allows us to remove the module from this application and create a standalone entity (akin to a microservice). This independent module can then be externally shipped and encapsulated within the project web, site, serverless architecture, etc., facilitating ongoing DevOps practices.

An important question arises:

How do we manage in-module communication post-extraction?

While an exhaustive discussion of this topic exceeds our current scope, our primary approach involves inter-module communication through APIs (such as endpoints, gRPC, SignalR) or leveraging contract interfaces for in-assembly communication. Asynchronous communication via a message broker is also a viable method.

Note

It's important to highlight that in-assembly communication poses a challenge, necessitating partial rewriting to accommodate different communication methodologies. However, these adjustments primarily concern implementation details.

As you can see, exploring Umbraco's Modular Monolith underscores its robustness in structuring a comprehensive project. The 'members' and 'articles' modules exemplify the architecture's adaptability, showcasing various architectural structures while emphasising encapsulation and internal visibility.

Should the modular monolith story stay untold?

I certainly don’t think so, and I hope that we explore it more as a community.

Modular monoliths and vertical slices narrates a saga of simplicity, adaptability, and future-proofing in software architecture. It's an ode to embracing structured yet flexible approaches that pave the way for streamlined development and evolution, ensuring systems remain agile amidst the ever-changing tech landscape.

Of course, this piece just scratches the surface of modular monoliths. 

There is so much more to explore: module communication, domain/context boundaries, and other technical intricacies that delve deeper into the workings of this architecture. The complexities of module communication, domain/context boundaries, and other technical nuances delve deeper into the intricate workings of this architecture.

I highly recommend Milan Jovanovic's tech blog if you’re in the mood for more modular monolith stuff. You can get lost for hours exploring the intricacies of modular monolith communication patterns, vertical slice architecture and the advantages of Clean Architecture in handling complex projects! Here are a few of my favourites: 

I hope this gives you a brief glimpse into the exciting possibilities of using modular monoliths, vertical slices and Umbraco. Maybe you already use them on your projects or want to know more? Contact me on Twitter/LinkedIn or head to https://www.wearecogworks.com/blog to explore the topic more.

Cheers,
Adrian