Published on

Modular Monolith - A Gentle Introduction

7 min read
Authors
Banner

Series

This post is part of a Modular Monolith series, in which we'll dive into the many facets associated with the architecture.

Introduction

When starting a new project, one of the first and most important decisions you'll have to make is choosing a Software Architecture. Choose an architecture that is too simple and the project will quickly outgrow it. Choose an architecture that is too complex, and you'll spend more time maintaining the architecture than building new features. Therefore, it's important to choose an architecture that is just right.

Introducing Modular Monoliths — "The Goldilocks Architecture".

Goldilocks

Architecture Comparison

Before we dive into Modular Monoliths, let's take a look at some of the other common architectures for context.

Clean Architecture

Clean Architecture is a monolithic architecture that emphasizes separation of concerns, making your system easier to maintain and scale. This architecture is designed to keep the business logic independent of the frameworks and tools, which helps in achieving a decoupled and testable codebase.

Clean Architecture slices features horizontally into layers:

  • Presentation: UI or API
  • Application: Orchestration of business logic
  • Domain: Business logic
  • Infrastructure: External dependencies such as databases, APIs, etc.
Clean Architecture

Vertical Slice Architecture

Vertical Slice Architecture is also a monolith architecture and structures your system around features rather than technical layers. Each feature is implemented end-to-end, including UI/API, business logic, and data access. A vertical slice may share common code with other slices, but the goal is to keep this to a minimum. This approach improves maintainability and reduces the risk of breaking changes.

For example, a User Registration feature could be implemented as follows:

  • UserRegistration folder
    • UserRegistrationEndpoint.cs
    • UserRegistrationHandler.cs (or UserRegistrationService.cs)
    • UserRegistrationRepository.cs
    • UserEntity.cs
Vertical Slice Architecture

Microservices

Microservices architecture is a distributed architecture involves splitting the application into small, independently deployable services. Each service focuses on a specific business capability and can be developed, deployed, and scaled independently. This approach is beneficial for complex and large-scale applications with multiple teams working on different parts. Internally, each service could be implemented using a monolithic architecture such as Clean Architecture or Vertical Slice Architecture.

Microservices

Although Microservices brings some powerful benefits, it also comes with its own set of challenges which I call Microservices Tax:

  • Complexity: Distributed systems are inherently more complex than monolithic systems
  • Development Velocity: Inter-service communication can be challenging and increase development times. Inter-team communication can also cause delays
  • Data Management: Data consistency and transactions become more complex
  • Deployment Complexity: Managing multiple services can be challenging, as versioning and deployment need to be carefully managed
  • Testing: End-to-end testing can be difficult
  • Resilience: Services need to be resilient to transient faults and service availability

Oftentimes, a Microservice is mistakenly seen as the only alternative to a Monolithic Architecture such as Clean Architecture or Vertical Slice Architecture. However, there is a middle-ground between a Monolith and Microservices — the Modular Monolith.

What is a Modular Monolith?

A Modular Monolith organises the system into modules that encapsulate specific functionalities. While it runs as a single application, it retains some benefits of Microservices, such as independent modules development and testing. It’s a good middle-ground between a monolith and microservices.

Each module should encapsulate a specific business capability, such as UserManagement, OrderManagement, ProductCatalog, etc. Similar to Microservices, a module should have minimal dependencies on other modules and only interact with them through well-defined interfaces. From an implementation point of view each module could be built using Clean Architecture or Vertical Slice Architecture (or perhaps even no architecture). Similarly, a Microservices Architecture could use Clean Architecture or Vertical Slice Architecture within each service.

Each module is also responsible for it's own data access, and could have it's own database and schema. Different modules can also use different data stores (e.g. SQL Server, CosmosDB, Redis, etc.) depending on the needs of the module.

Modular Monoliths and Microservices are both similar in that they provide a logically separated architecture. The key difference is that Modular Monoliths are physically co-located in a single process, whereas Microservices are physically separated into different processes.

Modular Monolith

NOTE: In the real world, I don't recommend using a database like Redis Cache as a primary data store for business-critical information such as orders. This is just an example to illustrate the concept of Modular Monoliths.

Code Structure

OK, I understand conceptually what a Modular Monolith is, but what does it look like in code?

Code Structure

In the code structure above, each module is a separate folder in the solution. I've chosen to use a Vertical Slice Architecture within each module, in the interests of simplicity and high-cohesion. You'll also notice I have a test project within each module, which is important to ensure each module is independently testable.

The WebApi project is an ASP.NET Core Web Application and is our host and entry point into the application. It will import all other modules as dependencies. This project is kept intentionally lean as the majority of the code should be contained within the modules.

Common.SharedKernel is a common library that can be used to share code between multiple modules. This could include things like base classes, interfaces, enums, etc. However, this should be used sparingly as it can lead to tight coupling between modules.

We'll dive deeper into the code structure in future posts.

Why Choose a Modular Monolith?

Alright, so we now know what a Modular Monolith is and how to structure our code from a high level, but why would you choose this architecture over Microservices?

The big benefit that Modular Monoliths provide is logical separation of code. This helps your monolith application to grow and scale without becoming a tangled mess of spaghetti code. It also allows you to incrementally transition to a Microservices architecture in the future, as each module can be extracted into its own service.

Another huge benefit is the ability to adjust module boundaries. This is one of the hardest things to get right in Microservices, and the biggest downfall as to why many Microservices architectures fail. With a Modular Monolith, you can combine modules that are too small or frequently communicate with each other.Likewise, you can also split modules that are too large or have too many dependencies.

Advantages Over Microservices

  1. Reduced Complexity: Due to a single process and reduction of inter-service communication over the network
  2. Improved Maintainability: Improved code organisation and maintainability
  3. Faster Development Velocity: Due to reduced complexity
  4. Easier Debugging: Easier testing and debugging due to a single process and the ability to step from one module to another
  5. Deployment Simplicity: Simplicity due to single deployment unit
  6. Reduced Versioning: No need to worry about API versioning, as all modules are in the same process and deployed together
  7. Increased Resilience: Less worrying about transient faults, service availability and networking issues
  8. Lower Cost: Reduced operational costs due to less infrastructure required

Real World Examples

There are many companies that have successfully implemented very large Modular Monoliths. To name a few:

Conclusion

Modular Monoliths are a great choice for striking a balance between development velocity and maintainability. Whether you’re working on a startup or an enterprise application, the Modular Monolith could be the "Goldilocks" architecture that is just right for your needs.

Within the realm of software development, different architectures pop in and out of vogue constantly. Keeping up with the trends can be exhausting, but choosing the right architecture is crucial for the longevity of your product. The Modular Monolith provides a trade-off without the compromise between the simplicity of a monolith and the logical separation of microservices.

Modular Monoliths allow you to structure your code with clear boundaries, making it easier to maintain, test, and evolve over time.

In the next post, we'll take a deep dive into the code structure of a Modular Monolith and explore how to implement one in a .NET application.

Resources