This post is part of articles series about Modular Monolith architecture:
1. Modular Monolith: A Primer
2. Modular Monolith: Architectural Drivers (this)
3. Modular Monolith: Architecture Enforcement
4. Modular Monolith: Integration Styles
5. Modular Monolith: Domain-Centric Design
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?
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.
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
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).
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
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.
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.
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 a distributed architecture, the matter is not so simple despite the technologies and tools (like Docker and Kubernetes) that facilitate this process.
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.
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.
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.
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 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.
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:
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.
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.
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.
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.
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.
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.
- 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
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