There are two ways of building complex systems — either using monolithic or microservices architecture. Which one to choose and when? Let’s figure it out!
Selecting and building the proper architecture is the number one task in a to-do list for software product development. Today, there’s a common opinion that monolithic architecture is something obsolete, while microservices are a cutting-edge technology. However, this isn’t true. Both architecture types can be safely used for different purposes, the only question is: how to pick the right one for a particular case? Let’s take a closer look at them to find the answer.
Let’s imagine we are building a complex application using monolithic architecture. This is what it looks like — all components exist as a single piece of code, each sharing a single database with the rest.
In the beginning, everything is great. The app is easy to build, deploy, and test as a monolithic piece of code.
However, successful products now live years and years, constantly growing in size and complexity. If you are planning a long and happy life for your app (and you definitely are), eventually, you may end up having a monolithic hell instead of a beautiful and easy-to-manage app.
Why is that so?
Apps with monolithic architecture are a set of tightly-coupled modules that are difficult to single out from a monolithic piece of code. That’s why it is quite challenging to:
- Update and extend separate modules — you’ll have to re-deploy the entire app every time you need to update a single module.
- Roll back when something went wrong — again, you’ll have to re-deploy the whole application to undo changes in one module.
- Test separate units of code — a monolith is really hard to split up into separate modules.
- Carry out code reviews — pieces of code are usually very large and therefore difficult to review.
- Manage a large team of developers — one day, you may find yourself unable to determine who is responsible for what piece of code.
- Scale the app horizontally — in other words, adding more inexpensive machines to scale high-loaded modules of the application. Horizontal scaling is usually associated with microservices; however, in the case of monolithic apps, it is hardly an option. As monolithic applications cannot be easily divided into separate services, you’ll have to scale the entire app each time there is a bottleneck in one module.
The points described above are enough to understand the main idea — monolithic architecture is a bad choice for constantly-evolving products.
Who’s gonna save us then? Microservices!
Functional decomposition is at the core of microservices architecture.
Let’s go back to our previous example. If we implemented the same app with the use of microservice architecture, it would look like what is shown in the picture below. What was previously a single piece of code are now separate services, each with its own database and a single entry point to all of them.
This is functional decomposition in action. What are its benefits though?
With microservices architecture, it’s much easier to:
- Work on a certain functionality and keep in mind its scope. In case of microservices, the scope of the app functionality is decomposed to the smallest possible parts (modules or services), which are much easier to handle than giant monolithic apps.
- Manage and review all changes made to the code as they take place in clearly separated services.
- Test services separately — as each of them is responsible for a different piece of functionality: you can test each one separately without affecting the others. A good choice for A/B and capacity tests.
- Update and deliver services separately, as well as manage partial releases and test the features that have already been released.
- Be flexible with innovative technology stacks — you can pick a new technology stack for each service.
- Manage responsibilities. Each team is responsible for a particular service and can develop, test and deploy the code independently and autonomously so that no one has to rely on other people’s progress. Moreover, as a bonus, you always know who to blame.
- Scale horizontally rather than vertically, without involving the entire system.
Sounds idyllic, right? However, is microservice architecture a silver bullet? The answer is no!
Below is the “ugly truth” about microservices:
- Distributed systems always bring a whole new set of problems. In case of microservices, for example, we have lots of independent services that need to communicate via HTTP, or messages, or something else, and this may cause a lot of trouble — from network issues to the message bus overflow, when messages can be lost and never delivered, to the wrong order of messages, etc. For reference: with monolithic architecture, communication between the services is simply calling the code inside the code.
- This is great because with microservices you do not have to stick to one technology stack and can always try something new when adding a new feature to the app. It helps you always stay up-to-date. However, the flip side of the coin is that the usage of new technologies should be under strict control. Otherwise, after a while, your app will look like a zoo where every service is developed with a new framework or even language. So, it’s better to have the essential languages, frameworks, and approaches standardized across the system and change them only when it is really required.
- The entire system should be designed resilient to small errors in case you don’t want the app to go down due to an error in a single service.
- The “unappealing” part of microservices is distributed transactions. When the app is a single piece of code, it usually has one database, and any request can be handled via a single database transaction. With microservices, the system is distributed — this means that every service has its own database and data consistency needs to be carefully maintained across the system.
Indeed, sometimes the need for distributed transactions is just the result of bad design, but there are still actual business cases requiring proper distributed transaction handling. There are several approaches to the effective handling of distributed transactions, such as event sourcing.
- With microservices, it’s really easy to release, scale, deploy, and test services separately, but to operate the entire system might be very complex. Microservices should be designed very carefully from the very beginning. Otherwise, after some time, you can find yourself in microservices hell, where all services are connected to each other and failure in a single service leads to complete system outage. Containers and container orchestration systems can be of great help when managing microservices.
Though neither monolithic nor microservices architecture is a foolproof approach, here is the main point. Despite the whole set of new issues that microservices bring us, actual solutions exist for almost all of them. In the case of monolithic architecture, there are not many ways to make your life easier, and you just need to suffer quietly. :)
In general, most apps in the world are getting more complex with time, and it’s hard to find an app that will always work perfectly with a small set of features and won’t grow over time. However, such apps do exist — and for them monolithic architecture is perfect!
So, there is no a one-size-fits-all solution. Both monolithic and microservices architectures have every right to exist — you just have to pick the right one according to your purposes.
Originally published at yellow.id.