Modular Monolith: Architectural Drivers

This post is part of articles series about Modular Monolith architecture:

1. Modular Monolith: A Primer
2. Modular Monolith: Architectural Drivers (this)

Introduction

In the first post about the architecture of Modular Monolith I focused on the definition of this architecture and the description of modularity. As a reminder, the Modular Monolith:

  • is a system that has exactly one deployment unit
  • is a explicit name for a Monolith system designed in a modular way
  • modularization means that module:
    – must be independent, autonomous
    – has everything necessary to provide desired functionality (separation by business area)
    – is encapsulated and has a well-defined interface/contract

In this post I would like to discuss some of the most popular, in my opinion, Architectural Drivers which can lead to either a Modular Monolith or Microservices architecture.

But what exactly are Architectural Drivers?

Architectural Drivers

In general, you can’t say that X architecture is better than the other. You can’t say that Monolith is better than Microservices, Clean Architecture is better than Layered Architecture, 3 layers are better/worse than 4 layers and so on.

The same rule applies to other considerations such as ORM vs raw SQL, “Current State” Persistence vs Event Sourcing, Anemic Domain Model vs Rich Domain Model, Object-Oriented Design vs Functional Programming… and a lot of more.

So how we can choose the architecture/approach/paradigm/tool/library if there is, unfortunately, no best?

Context is king

Each of our decisions are made in a given context. Each project is different (it results from the project definition) so each context is different. It implies that the same decision made in one context can bring great results, while in another it can cause devastating failure. For this reason, using other people’s/companies approaches without critical thinking can cause a lot of pain, wasted money and finally – the end of the project.

Project Contexts
Every Project is different and has different Context

However, context is a too general concept and we need something more specified to put into practice. That’s why the Architectural Drivers concept was defined. Michael Keeling writes about them in his blog article in the following way:

Architectural drivers are formally defined as the set of requirements that have significant influence over your architecture.

Simon Brown in the book Software Architecture for Developers describes Architectural Drivers similarly:

Regardless of the process that you follow (traditional and plan-driven vs lightweight and adaptive), there’s a set of common things that really drive, influence and shape the resulting software architecture.

Architectural Drivers have their categorization. The main categories are:

  • Fuctional Requirements – what and how problems does the system solve
  • Quality Attributes – a set of attributes that determine the quality of architecture like maintainability or scalability.
  • Technical Constraints – technology standards, tools limitations, team experience
  • Business Constraints – budget, hard deadline
Architectural Drivers
Architectural Drivers

Most importantly, all Architectural Drivers are connected to each other and often focus on one causes loss of another (trade-offs everywhere, unfortunately). Let’s consider this example.

You have some service that calculates some important thing (Functional Requirement) in 3 seconds (Quality Attribute – performance). A new requirement appears, calculation is more complex and takes now 5 seconds (Performance decreased). To go back to 3 seconds another technology could be used, but there is no time for it (Business Constraint – hard deadline) and nobody has used it in the company yet (Technical Constraint – team experience). The only option to increase performance is to move the calculation to the stored procedure, which decreases maintainability and readability (Quality Attributes).

Architectural Drivers example
Architectural Drivers example

As you can see, the software architecture is a continuous choice between one driver and another. There is no one “right” solution. There is No Silver Bullet.

With this in mind, let’s see some of the popular Architectural Drivers and attributes which are discussed during the considerations of Modular Monolith and Microservices architectures

Level of Complexity

At the beginning, let’s consider one of the greatest advantages of a Modular Monolith compared to distributed architectures – Complexity. The definition of complexity on the Wiki is as follows:

Complexity characterizes the behavior of a system or model whose components interact in multiple ways and follow local rules, meaning there is no reasonable higher instruction to define the various possible interactions. The term is generally used to characterize something with many parts where those parts interact with each other in multiple ways, culminating in a higher order of emergence greater than the sum of its parts.

As you see above, Complexity is about components and their interactions. In Modular Monolith architecture interactions between modules are simple because each module is located in the same process. This means that the module that wants to interact with another module:

  • Knows the exact address where he will direct the request and is sure that this address will not change
  • The request is just a method call, no network needed
  • The target module is always available
  • Security issue is not a concern
Modular Monolith Complexity
Complexity – Modular Monolith

On the other hand, consider distributed system architecture. In this architecture, the modules / services are located on other servers and communicate via the network. This means that when a service wants to communicate with another, it must deal with the following concerns:

  • It needs to get somehow address of target module, because it may be changed
  • Communication takes place via the network, which necessitates the use of special protocols like HTTP and serialization.
  • Network may be unavailable (CAP theorem)
  • Secure communication between modules must be ensured

Of course, You can find solutions for these issues. For example, to solve the addressing issue you can add Service Registry and implement Service Discovery pattern. However, it means adding more components and algorithms to the system so complexity rapidly increases.

To be aware of the scale of the problems generated by the Microservices architecture, I recommend that you familiarize yourself with the patterns that are used to solve them. The list is large, and most of them are not needed at all in the Monolith architecture.

Complexity - Distributed System
Complexity – Distributed System

In summary, the architecture of the Modular Monolith is definitely less complex than that of distributed systems. High complexity reduces maintainability, readability, observability. It needs an experienced team, advanced infrastructure, specific organizational culture and so on. If simplicity is your key architectural driver then consider Monolith First.

Productivity

The team’s productivity in delivering changes can be measured in two dimensions: in the context of the entire system and the single module.

In the context of the whole system, the matter is clear. The architecture of the Modular Monolith is less complex => the less complex the easier to understand => the productivity is higher.

From the point of view of the ease of running the entire system, the Modular Monolith ensures productivity at the maximum level – just download the code and run it on the local machine. In distributed architecture, the matter is not so simple despite the technologies and tools (like Docker and Kubernetes) that facilitate this process.

Running entire system – Monolith vs Distributed

On the other hand, we have productivity related to the development of a single module. In this case, the microservice architecture will be better, because we do not have to run the entire system to test one specific module.

Which architecture, then, supports the team’s productivity? In my opinion, for most systems, the Modular Monolith, but for really large projects (tens or hundreds of modules) are microservices. If your architectural driver is development speed and the system is not huge, a better choice will be a Modular Monolith and in case of system expansion, a possible transition to microservices could be right move to do.

Deployability

The deployability of a software system is the ease with which it can be taken from development to production. However, we must consider 2 situations here. The deployment of the entire system and the single module.

In the context of the entire system, is it easier to deploy one application of several applications? Of course, one application is easier to deploy so it seems that Modular Monolith is better option.

Deployment - Modular Monolith
Deployment – Modular Monolith

On the other hand, in a Modular Monolith, we always have to deploy the whole system. We can not deploy one particular module and this is one of the most important disadvantages. In this architecture, we do not have deployment autonomy so deployment process must be coordinated and may be more difficult.

Deployability - Distribiuted System
Deployability – Distribiuted System

In summary, if you do not mind the deployment of the whole system and you do not care about the autonomy of deployment- this is the point behind the Modular Monolith. Otherwise, consider distributed architecture.

Performance

Performance is about how fast something is, usually in terms of response time, duration of processing or latency.

Assuming the scenario that all requests are processed in a sequential manner, Monolith architecture will always be more efficient than a distributed system. All modules operate in the same process, so there is no overhead on communication between them.

The distributed system has overhead caused by communication over the network – serialization and deserialization, cryptography and speed of sending packets.

Even in real scenarios, the Monolith will be more efficient, but only for some time. With the increase in users, requests, data, and complexity of calculations, it may turn out that performance decreases. Then we come to one of the main drivers of the Microservices architecture: scalability.

Scalability

What is scalability? Wikipedia says:

Scalability is the property of a system to handle a growing amount of work by adding resources to the system

In other words, scalability is about the ability for software to deal with more requests or data.

It’s best to show this by example. Let’s assume that one of our modules must now handle more requests than we initially assumed. To do this, we must increase the resources that are responsible for the operation of this module.

We can always do it in two ways. Increase node computing power (called Vertical Scaling) or add new nodes (called Horizontal Scaling). Let’s see how it looks from the point of view of Monolith and Microservices architecture:

Scaling
Scaling

As can be seen above, both architectures can be scaled. Monolith can be scaled too. Vertical Scaling is the same, but the difference is in Horizontal Scaling. Using this approach, we can scale the Modular Monolith only as a whole, which leads to inefficient resource utilization. In the Microservices architecture, we scale only those modules that we need to scale, which leads to better utilization of resources. This is the main difference.

The more instances of the modules must work, the more significant the difference. On the other hand, if you don’t have to scale a lot, maybe you better accept less efficient resource utilization and stay with the Monolith and take its other advantages? This is a good question that we should ask ourselves in such a situation.

Failure impact

Sometimes our architectural driver may be limiting the impact of failure. Let’s say we have a very unstable module that crashes the entire process once in a while.

In the case of a Modular Monolith, as the whole system works in one process, the whole system suddenly stops working and our availability decreases.

In the case of Microservices architecture, the “risky” module can be moved into a separate process and if it is stopped the rest of the system will work properly.

Failure impact
Failure impact

To increase the availability of the Modular Monolith, you can increase the number of nodes, but as with scalability, resource utilization will not be at the highest level compared to Microservices architecture.

Heterogeneous Technology

One of the attributes of a Modular Monolith that cannot be bypassed in any way is the inability to use heterogeneous technology. The whole system is in the same process, which means that it must be running in the same runtime environment. This does not mean that it must be written in the same language because some platforms support multiple languages (for example .NET CLR or JAVA JVM). However, the use of completely separate technologies is not possible.

Heterogeneous Technology
Heterogeneous Technology

A feature of heterogeneous technology can be decisive to switch to Microservices architecture, but it doesn’t have to be. Often, companies use one technology stack and no one even thinks about the implementation of components in different technologies because team competence or software license does not allow it.

On the other hand, larger companies and projects more often use different technologies to maximize productivity using tailor-made tools to solve specific problems.

A common case associated with heterogeneous technology is the maintenance and development of the legacy system. The legacy system is often written in old technology (and often in a very bad way). To use the new technology, a new service/system is often created that implements new functionalities and the old system only delegates requests to the new one. Thanks to this, the development of the legacy system can be faster and it is easier to find people willing to work with it. The disadvantage here is that because of two systems instead of one – the whole system becomes distributed – with all of cons this architecture.

Summary

This post was not intended to describe all architectural drivers in favor of a Modular Monolith or Microservices. Separate books are being created on this topic.

In this post, I wanted to describe the most common discussed architectural drivers (in my opinion) and make it very clear that the shape of the architecture of our system is influenced by many factors and everything depends on our context.

Summarizing:

  • There is no better or worse architecture – it all depends on the context and Architectural Drivers
  • Architectural Drivers have their categorization – Functional Requirements, Quality Attributes, Technical Constraints, Business Constraints
  • Monolith architecture is less complex than a distributed system. Microservices architecture requires much more tools, libraries, components, team experience, infrastructure management and so on
  • At the beginning, the Monolith implementation will be more productive (Monolith first approach). Later, migration to Microservices architecture can be considered but only if architectural driver for that migration exists
  • Deployment of Monolith is easier but does not support autonomous deployment.
  • Both architectures supports scalability, but Microservices are way more efficient (resources utilization)
  • Monolith has better performance than Microservices until the need for scaling appears – then it depends on scaling possibilities
  • Failure impact is greater in Monolith because everything works in the same process. Risk can be mitigated by duplication but it will cost more than in Microservices architecture
  • Monolith from definition does not support heterogeneous technology

Additional Resources

1. Architectural Drivers – chapter from Designing Software Architectures: A Practical Approach book – Humberto Cervantes, Rick Kazman
2. Software Architecture for Developers book – Simon Brown
3. Design It! book – Michael Keeling
4. Collection of articles about Monolith and Microservices architectures named “When microservices fail…”
5. Modular Monolith with DDD – GitHub repository

Related Posts

1. Modular Monolith: A Primer

Modular Monolith: A Primer

This post is part of articles series about Modular Monolith architecture:

1. Modular Monolith: A Primer (this)
2. Modular Monolith: Architectural Drivers

Introduction

Many years have passed since the rise of the popularity of microservice architecture and it is still one of the main topics discussed in the context of the system architecture. The popularity of cloud solutions, containerization and advanced tools supporting the development and maintenance of distributed systems (such as Kubernetes) is even more conducive to this phenomenon.

Observing what is happening in the community, companies and during conversations with programmers, it can be concluded that most of the new projects are implemented using the microservice architecture. Moreover, some legacy systems are also moving towards this approach.

Ok, the subject of the post is Modular Monolith and I dwell on microservices, the question is why? Namely, because I think that as an IT industry we have made a false start adopting microservice architecture to such an extent. Instead of focusing on architectural drivers, we believed that microservices are medicine for all the evil that sits in monolithic applications. If you have participated in the development of a system that consists of more than one deployment unit, you already know that this is not the case. Each architecture has its pros and cons – microservices are no exception. They solve some problems by generating others in return.

With this entry, I would like to start a series of articles on the architecture of Modular Monolith. I do it for several reasons.

First of all, I would like to refute the myth that you cannot make a high-class system in monolithic architecture. Secondly, I would like to dispel doubts about the definition of this architecture and its appearance – many people interpret it differently. Thirdly, I treat this series of posts as an extension and addition to my implementation of Modular Monolith with DDD architecture, which I shared a few months ago on GitHub and which was very well received (1k stars a month after publication).

In this introductory post, I will focus on the definition of a Modular Monolith architecture.

What is Modular Monolith?

I always try to be precise when I talk or write about technical and business issues, especially when it comes to architecture. I believe that a clear and coherent message is very important. That is why I would like to clearly define what the architecture of the Modular Monolith means to me and how I perceive it.

Let’s start with the simpler concept, what is Monolith?

Monolith

Wikipedia describes “monolithic architecture” in terms of building construction and not computer science as follows:

Monolithic architecture describes buildings which are carved, cast or excavated from a single piece of material, historically from rock.

In terms of computer science, building is the system and the material is our executable code. So in Monolith Architecture, our system consists of exactly one piece of executable code and nothing more.

Let’s see 2 technical definitions: first one about Monolith System:

A software system is called “monolithic” if it has a monolithic architecture, in which functionally distinguishable aspects (for example data input and output, data processing, error handling, and the user interface) are all interwoven, rather than containing architecturally separate components.

Second one about Monolithic Architecture:

A monolithic architecture is the traditional unified model for the design of a software program. Monolithic, in this context, means composed all in one piece. Monolithic software is designed to be self-contained; components of the program are interconnected and interdependent rather than loosely coupled as is the case with modular software programs

These 2 definitions above (one of the first results in Google) have 2 shared assumptions.

First, they define that this architecture assumes that all parts of the system form one deployment unit – I will agree with that.

The second shared assumption of these definitions is that they assume a lack of modularity in such architecture and I will definitely disagree with that. The phrases “interwoven, rather than containing architecturally separate components” and “components of the program are interconnected and interdependent rather than loosely coupled” very negatively characterize this architecture, assuming that everything is mixed in them. It may be so, but it doesn’t have to be. It is not the ultimate attribute of the Monolith.

To sum up, Monolith is nothing more than a system that has exactly one deployment unit. No less no more.

Modularization

I’ve defined what Monolith means, let’s get to second aspect: Modularity.

What does it mean that something is modular according to the English Dictionary?

Consisting of separate parts that, when combined, form a complete whole/made from a set of separate parts that can be joined together to form a larger object

and Modularization itself:

The design or production of something in separate sections

Because it is a general definition, it is not enough for the programming world. Let’s use a more specific technical one about Modular programming:

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality. A module interface expresses the elements that are provided and required by the module. The elements defined in the interface are detectable by other modules. The implementation contains the working code that corresponds to the elements declared in the interface.

Several important issues have been raised here. In order to have modular architecture, you must have modules and these modules:

  • a) must be independent and interchangeable and
  • b) must have everything necessary to provide desired functionality and
  • c) must have defined interface

Let’s see what these assumptions mean.

Module must be independent and interchangeable

For the module to meet these assumptions, as the name implies, it should be independent. Of course, it is impossible for it to be completely independent because then it means that it does not integrate with other modules. The module will always depend on something, but dependencies should be kept to a minimum. According to the principle: Loose Coupling, Strong Cohesion.

In the diagram below on the left we have a module that has a lot of dependencies and you can definitely not say that it is independent. On the other hand, on the right, the situation is the opposite – the module contains a minimum of dependencies and they are more loose, it is finally more independent:

Module independence
Module independence

However, the number of dependencies is just one measure of how well our module is independent. The second measure is how strong the dependency is. In other words, do we call it very often using multiple methods or occasionally using one or a few methods?

Strong/Weak dependency
Strong/Weak dependency

In the first case, it is possible that we have defined the boundaries of our modules incorrectly and we should merge both modules if they are closely related:

Modules merged
Modules merged

The last attribute affecting the independence of the module is the frequency of changes of the modules on which it depends on. As you can guess – the less often they are changed, the more the module is independent. On the other hand, if changes are frequent – we must change our module often and it loses its independence:

Module stability
Module stability

To sum up, the module’s independence is determined by three main factors:

  • number of dependencies
  • strength of dependenies
  • stability of the modules on which the module depends on

Module must have everything necessary to provide desired functionality

The module is a very overloaded word and can be used in many contexts with different meanings. A common case here is to call logical layers as modules, e.g. GUI module, application logic module, database access module. Yes, in this context these are also modules but they provide technical, not business functionality.

Thinking about a module in a technical context, only technical changes cause exactly one module to change:

Technical modules and technical change
Technical modules and technical change

Adding or changing business functionality usually goes through all layers causing changes in each technical module:

Technical modules - new/change business feature
Technical modules – new/change business feature

The question we have to ask ourselves is: do we more often make changes related to the technical part of our system or changes in business functionality? In my opinion – definitely more often the latter. We rarely exchange the database access layer, logging library or GUI framework. For this reason, the module in the Modular Monolith is a business module that is able to fully provide a set of desired features. This kind of design is called “Vertical Slices” and we group these slices in the module:

Business modules and vertical slices
Business modules and vertical slices

In this way, frequent changes affect only one module – it becomes more independent, autonomous and is able to provide functionality by itself.

Module must have defined interface

The last attribute of modularity is a well-defined interface. We can’t talk about modular architecture if our modules don’t have a Contract:

Modules without contract (interface)
Modules without contract (interface)

A Contract is what we make available outside so it is very important. It is an “entry point” to our module. Good Contract should be unambiguous and contain only what clients of a given contract need. We should keep it stable (to not break our clients) and hide everything else behind it (Encapsulation):

Modules with contract
Modules with contract

As you can see in the diagram above, the contract of our module can take different forms. Sometimes it is some kind of facade for synchronous calls (e.g. public method or REST service), sometimes it can be an published event for asynchronous communication. In any case, everything that we share outside becomes the public API of the module. Therefore, encapsulation is an inseparable element of modularity.

Summary

1. Monolith is a system that has exactly one deployment unit.
2. Monolith architecture does not imply that the system is poor designed, not modular or bad. It does not say anything about quality.
3. Modular Monolith architecture is a explicit name for a Monolith system designed in a modular way.
4. To achieve a high level of modularization each module must be independent, has everything necessary to provide desired functionality (separation by business area), encapsulated and have a well-defined interface/contract.

In the next post I will discuss the pros and cons of Modular Monolith architecture comparing it to the microservices.

Additional resources

1. Modular Monoliths Video – Simon Brown
2. Majestic Modular Monliths – Axel Fontaine
3. Modular programming – Wikipedia
4. Monolithic application – Wikipedia
5. Modular Monolith with DDD – GitHub repository
6. Vertical Slice Architecture – Jimmy Bogard

Related posts

1. GRASP – General Responsibility Assignment Software Patterns Explained
2. Attributes of Clean Domain Model
3. Domain Model Encapsulation and PI with Entity Framework 2.2
4. Simple CQRS implementation with raw SQL and DDD

Image credits: Magnasoma