Let me announce contenders. In the red corner is a former title champion in a super heavyweight category — Mr. Monolith. In the blue corner is a young and pumped-up gang of microservices in a featherweight category.
I saw several serious monoliths in my life. One of them had about 5+ hours of build time, and the company had to build their own CI to keep moving forward.
On the other hand, I saw people using a microservices architecture. They needed 17 load balancers to get their pre-Alpha application up and running. The fun part is that it was before the time of infrastructure as a code, so it was a really high number.
If you asked me about five years ago, I would say by default that the monolith is evil and microservices are good (and I would quote all standard things). However, the hands-on experience of these years taught me that going the microservices route may easily backfire.
Let me skip talking about monolith problems (it doesn’t make sense to beat the dead horse). And let me concentrate a bit on the issues with microservices.
- There are many things beyond coding that need to be done to have a product ready (making a build, running tests, deploying, monitoring, scaling, etc.). All of this needs to be figured out just once for a monolith. However, it needs to be repeated for each of the microservices. In the best case, your microservices are based on a similar technology stack to reuse it. In the worst case, you will need to reinvent the wheel for each of them. (A small note: Some of these pains are gradually solved by tooling, but only some of them).
- Who should wake up at 2 am if your service started to limp? A dedicated Ops team for the monolith usually has enough knowledge to solve the first level of problems (and allow you to handle the rest at 8 am). As soon as you have hundreds of microservices, you end up quite often with a couple of people (or maybe even just one) who know how to keep that specific service alive. It is not fun to be woken up at that 2 am just because some monitoring had a false positive. Also, this incredibly increases operational risks (what happens when that one person who knows some specific microservice is on vacation in someplace without an internet?)
- Moving code within monolith and changing interfaces generally isn’t that bad. Moving code between microservices and changing interfaces (including handling versioning and backward compatibility) is incredibly expensive.
- This interface change story becomes especially painful if you didn’t partition your application perfectly the first time around and find that some pieces of logic belong to the wrong microservice. And I can promise, you didn’t partition it perfectly. It would be naive to assume that initial thinking (which probably was implemented on day one) will be perfectly correct for subsequent years.
- Monolith uses the same language, framework, and so on. As a result, moving people between parts of a monolith is easy. Moving people between different microservices (which may have very different tech stacks) is hard.
- Debugging something across a set of microservices is painful, to say the least. Anybody who had to chase a bug across 4–5 microservices back and forth for days, to find that one of them didn’t do some validation would understand.
I am pretty sure there are a lot of other cons. However, even this list makes me question microservices as a panacea.
Ok. Monolith is bad. Microservices are bad… Ahhhh… Everything is bad. There is nowhere to run. What do we do?
The biggest problem is not monolith per se (or microservices per se), but rather extremes that bring about. And unfortunately, teams/companies may not recognize problems in time and start addressing them until they get completely out of hand.
Monolith becomes big and tends to continue to grow and worsen. When you realize it’s a problem, it’s already so big and hairy that breaking it down becomes an almost impossible task.
Microservices are often used prematurely, they grow in numbers and complexity of their interactions., And as a result, it brings all the problems which I described above.
My take on it, you need to have right-size-services © :) First of all, I believe the good idea is to start with reasonably monolithic architecture (one or a couple of services). As they began to grow, look for natural cracks (pieces of code with very different attributes). These is the pieces that could be gradually separated into standalone services.
To give you an example. I was working on some back-end service, and we found that one part was called way more often than others. It needs to scale horizontally way more aggressively than the rest. Also, we found that the current architecture for this piece was a bit problematic, and it had a reasonably small amount of dependencies on the rest of the backend. Well. Here you have a perfect candidate to be extracted as a separate service.
Two additional warnings/notes.
It would help if you kept an eye on the monolith. It’s easy to miss the point when it gets out of hand. It’s better to start separating things a bit premature than doing it too late. My gut feeling is that as soon as you begin spending around 5% of your time fighting monolith problems, it’s time to do something about it.
A lot of companies are very resistant to non-trivial refactoring efforts. It comes from multiple places. Engineers always like to refactor things whether it’s necessary or not. As a result, management is pretty skeptical of this. Additionally, big refactoring or rewrites increase risk (which managers are not fond of). I would recommend you start preparing upfront (taking about benefits, stages, minimization of risks, and so on). This way it allows to build consensus and be ready to execute.
It’s hard to tell who wins in this fight between monolith and microservices. Pretty much, you have to balance between this too, starting with simple (monolith) and moving towards separating complexities (microservices)