The title of this article is a bit of an exaggeration. Monoliths are evil and not so evil at the same time. How is that possible? Let’s find out.
Every application starts simple, so monolith architecture is the right choice. When the project starts there is often very little information available, so the project should always start with the most straightforward approach. However, one thing to keep in mind is that the project is bound to grow with the introduction of more features. Therefore, the projects need to be architected with the ultimate goal of Microservices in mind.
Modular architecture design following the Single Responsibility Principle can help us achieve this goal. Boundaries between different classes and services need to be along functional aspects. Upon neglection of these principles, the once simple monolithic application eventually becomes a tangled spaghetti.
Organizations often focus on adopting the best agile processes and practices. However, after a while, they realize that engineers are still struggling with meeting deadlines. The rate of feature development and release is prolonged, what they fail to realize is that the application is suffering from monolith fever, therefore no matter what they do to improve the process they are only putting band-aids rather than fixing the core problem.
Monolithic applications have some advantages and disadvantages. However, its disadvantages are far more than the advantages when it comes to large and complex projects.
Monolithic applications are tightly coupled with the technology stack. Therefore as the time passes and the technology becomes obsolete, it is challenging to upgrade it to use the latest technology frameworks or even upgrade the version of an existing framework if the new version is not backward compatible. This necessitates a rewrite of the complete application from scratch using the latest tech stack.
Because of the obsolete technology, it is difficult to find talent and resources who are interested in working on the old tech stack. Often to work on those applications engineers have to learn old technologies which does not help them much in their career advancements. It is far easier to recruit engineers who want to learn and apply the most cutting edge technologies.
Monolithic applications are tough to scale; they often have one centralized database which is a single point of failure. If that database is down or struggling due to heavy load the only way to scale it without additional development work is to use more powerful hardware, i.e. vertical scaling. The nonresponsive database becomes a bottleneck and in turn, decommissions the whole application. To handle increased load more application instances can be launched and added to the Load Balancer.
Another major problem with monolith applications is that there is no way to use optimized hardware based on feature requirements. If one feature is memory intensive whereas another feature is computationally intensive, then the monolithic application is out of luck as it can only utilize general purpose hardware.
As monolithic applications tend to be large and complex their testing become very error-prone and challenging. Engineers are scared to make any significant refactorings because it becomes very time consuming and tedious to figure out all paths that are affected by the refactored code. This causes a ripple effect, creating more code debt.
Deployment cycle is yet another victim of Monolithic architecture. Since the application is hard to test, even a minor change in the application requires a complete retest. Testing takes longer, causing the deployment cycle to prolong.
As the code base of the monolith is quite extensive, the learning curve is pretty steep, which in turn affects the productivity of the whole team. No single engineer can become SME of the application.
Larger applications take a long time to build and start, thus development time and feedback cycle prolongs. Continuous integrations take a long time as well, and if the build fails, it becomes very time consuming to figure out the cause.
To avoid breaking the build, developers use the branching strategy to work on the features; this becomes very problematic when they merge their branches to the master branch. Merges are error-prone, so instead of helping, it hurts them instead.
If a nasty bug creeps in such as memory leak, it affects the whole application thus causing production outages and affecting customers.
So in short monolithic architecture violates all the requirements of the modern software application. i.e.
Monolithic architecture is an excellent choice for small applications because they offer several advantages.
Monolithic applications can catch most of the bugs at compile time since they only have binary dependencies and no dependency on external services.
Monolithic applications are inherently more secure as the surface area for the attack is minimal and is centralized. So enforcing security is more natural.
The deployment procedure is very straight forward and only requires the deployment of one artifact across all instances.
For monolithic applications, setting up the development environment is very straight forward. Usually, only one code repository needs to be checked out, and IDE has access to all the code base of the application.
Debugging is easier in general, as the engineer can step through the code quickly without having to worry about external calls.
Transaction Management is very easy to implement in a monolithic architecture, since it is only dealing with a single database.
Testing monolithic application is simple, as there are no external dependencies to mock. Setting up the data required to run the test is straight forward as there is only one database involved.
To perform a similar task, monolithic architecture can be more performant as they only deal with local API calls, rather than making an over the network call to fetch the equivalent data. However, this advantage is at the cost of scalability.
I would like to set the stage for this article with an opening sentence.
Microservice architecture is not a swiss army knife
Understanding this phrase will make us ready to dive into the world of Microservices and discover its advantages as well as drawbacks. This phrase will help us perceive if Microservices Architecture is the right choice for our next project.
Microservices architecture is a way of architecting software applications involving multiple self-contained services. Each service provides specific business functionality and follows its own development and deployment cycle. This is synonymous to the Lego bricks which can be connected to create different objects. Similarly, multiple microservices can be mashed up to create an application that provides a more concrete value to the end users.
Microservices thus offer a reusable infrastructure which can be used in different contexts, e.g., a Messaging app encapsulates messaging service so it can be used with a Banking App, eCommerce App, Ticketing App, etc. It encapsulates the messaging domain so that other applications don’t have to worry about re-inventing the wheel for the messaging system.
Microservices are genuinely independent of each other; they are loosely coupled without any binary dependency between them. They only use each other’s services defined by messaging protocols or external API contracts.
Each of these services can evolve independently, one thing to keep in mind is that these services need to be evolved in a backward compatible way. If care is not taken, it will create a situation where other microservices need to be deployed in lock-step.
Microservices are small, easily manageable applications; therefore it is easy for engineers to get up to speed and be able to contribute without understanding the overall applications architecture. If the whole system is reasonably complex, this also helps with creating agile teams that focus on particular microservices.
Microservices are easy to test, writing the end to end acceptance or integration tests do not require a pile of data to be set up. Each microservice focuses on its domain; therefore data setup for testing is relatively straight forward.
Each properly crafted microservice has its database, which is chosen based on the requirements for that particular domain. Significant schema changes in one microservice do not affect other microservices. This helps tremendously with fewer down times.
If one microservice is hit with the outage, it does not have a significant impact on the usability of the entire system, as other microservices continue to provide their services. This, however, may not be true in case of a microservice which is central to the system such as Authentication App.
Each microservices can use the technology stack, including development language, frameworks, databases, etc suitable for solving problems for that particular domain.
In terms of security, even though microservices have a larger surface area that needs to be secured, but even in case of a breach, only the data managed by that microservice is at risk. Other microservices are not affected as they either have their separate databases or if they share the same database, they will have a different schema and credentials.
Best of all each service can be deployed on the most optimized hardware for that service. If one service requires more memory whereas others require more CPU, then they can each be deployed on the hardware which fulfills those requirements.
Microservices have a fairly small footprint; therefore it takes less time to build and launch, this helps engineers tremendously, giving them the ability to make changes and get much quicker feedback.
Deployment of microservices is effortless, provided they are enhanced in a backward compatible fashion. So utmost attention should be paid not to make changes which will break the contract with external clients, as it will require external clients to be upgraded in lockstep.
Microservices applications are easy to scale, it is easy to identify highly utilized services and focus on scaling just those services rather than scaling the whole infrastructure.
So far we were focused entirely on the good parts of microservices. However, as I mentioned early, it’s not a one size fits all architecture. It is well suited for some applications but not for others.
One issue is with designing the microservices, as it is difficult to come up with a microservice with well-defined boundaries. If the microservices are not crafted with the correct boundaries, they can drive the application towards a big, and messy dependency web. Where each microservice depends on various other services which in turn depend on more services and so on, microservices need to be modularized based on the business domain or functional boundaries which have minimal dependency on each other.
Transaction management across multiple services is the most challenging part of the microservice architecture. Imagine on an e-commerce website, a customer places an order by calling order management microservice, and payment processing is handled by some other microservice. What will happen if the order is placed successfully, but the payment processing fails. There needs to be appropriate infrastructure in place to reverse the order if the payment fails, which of course would be more challenging to handle compared to transactions handled at the database level.
If we look at the overall application comprising of multiple microservices, there are more moving parts, so a lot more places where things can go wrong.
Debugging an issue in microservices based platforms is also exceptionally difficult. The request has to travel through multiple microservices, so pinpointing the exact location where an error or exception occurred is cumbersome.
Hopefully, the advantages and disadvantages outlined in this article will help the reader decide which architecture to choose when designing an application.