[Software Architecture: The Hard Parts][Chapter 2] Architectural Modularity
This chapter focuses mainly on architectural modularity. As businesses evolve and face the torrent of evolution, the market pace also increases drastically. Technical environments also undergo rapid and fast-paced changes and architecturally, it's very difficult to keep up with these kinds of changes.
Naturally monolithic applications don't provide the level of scalability, agility and extensibility required to support for example acquisitions and merges. This brings us to an important part that states that capacity for single machines (CPU, memory) fills up very quickly. Acquisition and consumer demands keep increasing the size of monolithic applications to a point where a single instance might need a very expensive piece of hardware to run on. Take the example below where on the rightmost side is a monolithic application trying to keep up with market demands. As it grows spawning extra instances will spawn full cups as well making it very difficult to move forward.
Whereas if we broke the full glass of water into 2 (or more) parts, half the water can be poured elsewhere which can give more capacity and scalability.
Not only scalability but splitting monolithic applications gives us more agility which is the speed-to-arrive to market and respond quickly to change. One of the most important ways to survive in today's market is to have high agility. In the next section we will discuss the modularity drivers or in other words, why do we need to break down a large monolithic system into smaller parts?
One of the primary business drivers of architectural modularity is speed-to-market as mentioned above. It can give you a competitive advantage in the market. It's usually achieved with architectural agility which is a characteristic made up of other architectural characteristics; maintainability, testability and deployability.
Competitive advantage is achieved through agility combined with scalability and fault tolerance.
The five characteristics to support agility and speed-to-market are the following:
Availability (fault tolerance)
We'll go through each one and discuss more about it in the next section. Modularity doesn't necessarily mean splitting the system into a distributed architecture. It can still be achieved using modular monolith or micro-kernel architectures. In modular monolith, components are grouped into well-formed domains and in micro-kernel components are plug-in providing better testability and deployability.
Maintainability is the ease of adding, changing or removing features as well as any upgrades or such.
Some of the metrics used to determine the maintainability level of applications are based on their components and are as follows:
Component coupling is the degree to which components know about one another
Component Cohesion is the degree to which the operations of a component interrelate
Cyclomatic Complexity is the overall indirection and nesting within a component
Component size is the number of aggregated statements inside a component
Technical versus domain partitioning is answering whether components are aligned by technical usage or domain partitioning
A component is a building block of an application that does some sort of business or infrastructure function. Usually, a bunch of classes and functions are in some namespace or directory in the codebase.
Large monolithic applications have weak maintainability due to the technical partitioning of the layered architecture. Tightly coupling the components and having a weak component cohesion from a domain perspective.
A simple addition or deletion of a feature requires going through each layer whether it be persistence, business or presentation and making the changes accordingly. This high coordination requirement is not the best thing. Since the feature domain is spread out over the whole architecture. Having to test all the layers and deploy them all at once risking breaking anything else.
Whereas having modular domains enables us to make a change only at the domain level mitigating different risks in other parts of the application. So a service might own a certain domain making any change only restricted to this service alone.
So as modularity increases so does maintainability.
Testability is the ease of testing as well as the completeness of testing.
It is essential for agility, Large monolithic applications have very low testability because as mentioned above a simple change requires running the whole application suite of tests before deploying which can be hundreds of tests. Having to understand why a bunch of unrelated tests failed in the middle of all this can cause headaches.
Modularity reduces the testing scope of services which allows better completeness and ease of testing. But it's not always like that because as coupling increases between services, testability can become more complex since it now depends on other services as well. An important point to take into consideration of.
We can define deployability as the ease, frequency and overall risk of deployments. To support agility applications must support these factors. Deploying every two weeks increases the risk of deployment due to grouping lots of changes. Not only this but delays important bug fixes and changes that customers might need so it does impact them as well in some way.
Large Monolithics have low deployability due to the amount of ceremony required (code freezes, mock deployments, etc) that's why most Monolithics have a long frame between deployments. Modularity allows for less risk, and less ceremony and allows us to have more frequent deployments.
But not everything is a piece of cake, sometimes small service deployability can be a headache because as services communicate between each other and a service breaks, any other service communicating to it will have issues as well so it can cause a ripple effect causing the whole system potentially to shut down.
If your microservices must be deployed as a complete set in a specific order, please put it back into a monolith and save yourself some pain
Scalability is the ability of a system to remain responsive under a higher-than-usual user load. A term related to it is elasticity which means the ability of a system to withstand a burst increase in user load. (high spike)
Scalability occurs over a long period while elasticity is a sudden spike.
A good example is a concert ticketing system where between major events there is a light concurrent user load however when tickets go on sale the concurrent user load significantly spikes. To maintain spikes the system must have the ability to quickly start up more instances instantly. Elasticity relies on services having a small mean time to start (MTTS) which can be achieved using fine-grained services (less code to compile, faster startup, etc)
Elasticity is concerned the most with service granularity (size of the deployment unit), whereas scalability is more a function of modularity.
Large monolithic applications are very difficult to scale because they all scale as one unit which can be very costly in cloud-based infrastructures. Not to mention the poor time to start which affects elasticity as well.
In a service-based architecture, domain services offer much better scalability but not much elasticity as they are very coarse-grained services having domain functions.
Microservices however are finer-grained in nature and offer maximum elasticity and scalability.
Fault tolerance is the ability of some parts of the system to remain responsive and available as other parts of the system fail.
Large monolithic systems offer very low levels of fault tolerance because an exception or error in one part of the code affects the whole runtime.
Modularity can boost the overall fault tolerance of the system where multiple deployments exist and failures are isolated to one deployment only, allowing the rest of the system to function normally. But if other services are synchronously dependent on this service then fault tolerance is not achieved because as mentioned before the whole system potentially can fail. This is why asynchronous communication between services is recommended for maintaining high levels of fault tolerance.
Modularity drivers are answers to whether you need to add modularity to your existing monolithic application or not. The answer is always it depends. If you lack in any or all of the drivers mentioned it's maybe time to re-think the architecture of your application! In the next chapter, we'll be talking about Component decomposition patterns which are the first steps in breaking a large monolithic and achieving modularity. Till the next one!
Did you find this article valuable?
Support Amr Elhewy by becoming a sponsor. Any amount is appreciated!