In this article, we are going to discuss the monolith and distributed monolith architectural patterns. We’ll cover what the patterns are, how to implement them in C#, and why we should use them over some other alternatives such as microservices.

Let’s start with a basic introduction to the monolith pattern.

To download the source code for this article, you can visit our GitHub repository.

What Is a Monolith Architecture?

In Monolith Architecture, we deploy the entire application stack as a single executable on one or many servers.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Let’s look at a diagram to explain:

Monolith Architecture Diagram

This image demonstrates a common application stack comprising a Presentation Layer, a Business Logic Layer, and a Data Access Layer. Even though these may exist as separate classes or projects, we deploy them as a single application, and this characteristic defines the monolithic nature of this pattern.

For a bit of history, the term “monolith” comes from the Greek words monos (meaning “single”) and lithos (meaning “stone”). The reference here is that the application is a single large structure.

Let’s now discuss what a monolith architecture can look like in a C# application.

Project Structure

Let’s examine a typical monolith project structure:

Monolith Architecture Diagram

Here, we represent it as a simple application with the three layers we discussed previously. In this example, we are creating a music application that returns artists and albums. Here’s what the /artists endpoint returns:

[
  {
    "id": 1,
    "name": "The Black Keys",
    "genres": [
      "Rock",
      "Blues",
      "Alternative"
    ],
    "albums": [
      {
        "id": 1,
        "name": "The Big Come Up",
        "yearReleased": 2002
      },
      {
        "id": 2,
        "name": "Thickfreakness",
        "yearReleased": 2003
      }
    ]
  },
  ... //the rest is removed for the readability reasons
]

It’s worth noting we could also split out the layers into separate class libraries, and reference them from the main project. It still retains the classification of a “monolith” because the project can run as a single executable. But for our purposes, let’s keep it as a simple single project.

Advantages of the Monolith Architecture

The monolith architectural pattern is appealing for a variety of reasons. The first is simplicity and maintenance. It’s easy to create as it’s a single project, in a single codebase. This extends to developer productivity, as it’s simple for a new developer to come on board and run, debug, and test the entire application with the click of a button.

Deploying a monolith is also straightforward, as it involves hosting a single application on a single server. If we need to scale out, we can easily do that by duplicating the application across one or more additional servers.

At the launch of our project, we often don’t know the performance, cost or scalability needs for our application. This is something that comes with time. For this reason, when building an application for the first time, experts recommend starting with a monolith to avoid overengineering a solution that might hinder progress before reaping the benefits of complexity.

Disadvantages of the Monolith Architecture

As the application gets more complex, more lines of code are added, more dependencies are introduced, and it starts to become tough to deal with a monolith. Building the application takes longer, it takes longer to start, and if it’s not designed correctly it can be a tangled mess of spaghetti code. 

Because it’s a single application, it also means it’s a single point of failure. That is, a problem with one part of the code can bring down the entire application, meaning we don’t meet a high level of reliability or any kind of graceful degradation. This is where we start to venture into the world of alternative patterns.

Monolith vs Microservices

Because we are discussing monolith architecture, it’s worth noting the obvious comparison to another popular architectural pattern, microservices. The primary distinction lies in microservices breaking down the monolith into a set of independent components that can run, be hosted, and scale separately. The secondary reason a lot of teams move to microservices is because the number of developers increases, and therefore there is a higher chance of conflict in the single codebase of a monolith. Therefore, moving to separate microservices (and codebases) allows the developers to work autonomously and become more productive.

We highlighted that we may not always know our application use cases early on, which is why the journey toward microservices is often initiated at a later stage when these requirements and pain points are better understood. However, there is a first step we can take to gain some of the benefits without all the complexity, and that is the distributed monolith.

Distributed Monolith Architecture

Our current project structure is a single application with a few folders organizing the areas. What if we decided that the application was too complex to manage? Let’s say we wanted to expose some new functionality, perhaps to introduce concerts. Should this logic be considered a separate concern for artists and albums? Does it belong in the current application? However, maybe we want to share the same underlying data store and business layers. This is where the distributed monolith comes in.

Let’s first discuss what this architecture will look like:

Distributed Monolith Diagram

We now have two applications that share the data access layer and business logic layer, but they have independent presentation layers. This means the UI (or in our case, the API endpoints) can evolve independently. Remember we could share as little or as much as we want, but for our purposes let’s share just the data access and business logic layers.

In the next section let’s refactor our monolith to suit the new architecture.

Updating Our Project to Be a Distributed Monolith

First off, let’s create two new ASP.NET Core APIs called “ArtistsApplication” and “ConcertsApplication”, and then two new class libraries called “Data” and “Business”.

Let’s then refactor the respective code from the Monolith into the class libraries and the “ArtistsApplication”, making sure we adjust namespaces, add the necessary project references, then finally remove the monolith.

We should end up with a solution structure like this:

Distributed Monolith Project Structure

If we run the “ArtistsApplication” and hit the /artists endpoint things should still work as previously.

What have we done so far? We have broken apart the monolith into two separate applications, whilst sharing some common code. This is often a good first step to microservices, because now if we wanted one of these applications to be truly independent, we are already part of the way there, we only need to remove the coupling.

Adding New Functionality

Now, let’s add some functionality to our “ConcertsApplication”. Let’s first add a new model to the Business Layer called Concert:

public record Concert(int ArtistId, string Country, DateOnly Date);

Now let’s modify our MusicRepository:

public class MusicRepository
{
    private readonly IEnumerable<Artist> _artists = new[]
    {
        new Artist(1, "The Black Keys", ["Rock", "Blues", "Alternative"], 
            [new Album(1, "The Big Come Up", 2002), new Album(2, "Thickfreakness", 2003)]),
        new Artist(2, "Eric Prydz", ["Dance", "Electronic"], 
            [new Album(3, "Pryda", 2012), new Album(4, "Opus", 2016)]),
        new Artist(3, "Sam Cooke", ["Soul", "Gospel"], 
            [new Album(5, "Twistin the Night Away", 1962)])
    };

    private readonly IEnumerable<Concert> _concerts = new[]
    {
        new Concert(1, "United States", new DateOnly(2024, 6, 1)),
        new Concert(2, "Australia", new DateOnly(2024, 5, 1))
    };

    public Artist? GetArtistById(int Id) => _artists.SingleOrDefault(artist => artist.Id == Id);

    public IEnumerable<Artist> GetAllArtists() => _artists;

    public IEnumerable<Concert> GetAllConcerts() => _concerts;
}

Finally, let’s replace the WeatherController in the “ConcertsApplication” with some of our code:

[ApiController]
[Route("[controller]")]
public class ConcertsController(MusicRepository musicRepository) : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<Concert>> Get() => Ok(musicRepository.GetAllConcerts());
}

If we run the application and hit the /concerts application we see the data we created:

[
  {
    "artistId": 1,
    "country": "United States",
    "date": "2024-06-01"
  },
  {
    "artistId": 2,
    "country": "Australia",
    "date": "2024-05-01"
  }
]

Downsides of the Distributed Monolith Architecture

So far we have two partially independent applications, still in the same codebase, and the same solution, that are relatively easy to run and maintain. Things are great, right? As our application matures, and new requirements are created, things can become a problem.

For example, let’s say we want to change the Data Layer, perhaps opting for SQL Server. This means we need to update both applications. Similarly, what if we wanted to introduce a NuGet dependency into the Business Layer? This would mean that both applications would include a transitive dependency, increasing the end size and potentially introducing dependency issues.

This is where the argument starts to come for microservices.  If the applications were truly separate there would be no shared data layer or business layer, each application would have its own. Some logic might be duplicated, but at least they are truly independent and they can be upgraded by themselves.

Another disadvantage is scalability. Let’s say the GetConcerts() method was particularly slow, and we wanted to scale up/out this code. We would have to do this on both applications, even though the “ArtistsApplication” doesn’t care about that functionality. If they were separate, the “ConcertsApplication” could be scaled independently and therefore we would save some costs.

This is often where the balancing act comes in, where we need to decide on what’s more important: performance/scalability concerns, or simplicity and maintenance. We want everything of course, but like with all software there are tradeoffs in every choice.

Conclusion

In this article, we discussed the monolith and distributed monolith architectural patterns. Good architecture should be purpose-fit but also future-proof to the best of our knowledge. For this reason, despite the popularity of microservices, we are seeing a resurgence in a well-designed monolith, using modular concepts and distributed monoliths like we discussed today, to gain some of the benefits of microservices without all the complexity. This allows us to take our time and be pragmatic about our decisions, and only take the step into microservices if we have the demonstrated needs and resources to support this kind of complexity. Happy coding!

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!