Monolithic vs Microservices Architecture: An Overview in Plain Language
I recently wrote about this topic as a report for my work. We were looking ahead to rebuilding our company's internal software application and a consultant we'd hired had recommended the microservices architecture.
But I wanted to do a deeper investigation into the pros and cons to get a better understanding of whether or not it made sense for us.
I'm thankful I did because, judging from what I found, going with microservices would have caused us a lot of pain.
Monolithic Backend
A monolith is a unified backend architecture where all modules, functions, and services are bundled and deployed together as part of a single codebase. Typically it is connected with a unified (ie. monolithic) database as well.
Advantages
Keeping everything together makes shared operations like logging, error handling, performance monitoring, and deployments much simpler.
Debugging and testing is more streamlined since everything is together in one place.
Overall development is more straightforward since it’s the more common and familiar style of architecture most developers are used to.
Disadvantages
If one module or section of the application is down, that means the whole application is down.
Deploying a change for one small part of the application still requires a new deployment of the whole application.
Scalability options are more limited. You don’t have the option to scale or provide more resources to one module that needs it. You can only scale up the whole application as one.
Working with multiple teams of developers on a monolith can require a lot of coordination to make sure no one is interfering in any way with anyone else's work.
Microservices
With microservices the application is broken down into its component parts and a completely independent microservice is built for each one, each typically with its own individual database as well (but not necessarily). An API Gateway is then built to make requests to each of the services. The frontend client makes all its requests to the Gateway, not to any of the microservices directly.
It is extremely crucial when using a microservices architecture that each module/service is separate so that each one can be dealt with independently of the others. Or else all you get is the added complexity and none of the benefits.
Advantages
Each module can be updated, deployed, and scaled up independently.
If something brings down one of the modules, only that module will be affected. The rest of the application will still be okay.
When working with multiple teams, each team can focus on a microservice independently of the other teams. They can even work with a different tech stack than the others if they want.
Disadvantages
Increased complexity figuring out how the microservices will communicate and connect with each other.
Drawing the lines of separation between microservices is not always straightforward.
Organizing deployments is also more complicated because each module has to be deployed and served independently.
Shared operations like error logging, performance monitoring, testing, and any other shared configurations are more complicated, since you're dealing with many separate units as opposed to just one.
Service costs can increase since payments for hosting, performance monitoring, test runs, and other similar services are multiplied across multiple modules, as opposed to the single cost when everything is together.
Key Considerations / Questions
The danger of creating a distributed monolith.
Microservices only work if each microservice is completely independent. This requires a very careful design and detailed understanding of the system's modules and the actions they’re performing.
If the microservices become closely coupled with each other (one module sharing dependencies with another), you get a distributed monolith, which is the worst of both worlds. It means all the complexity of maintaining multiple microservices with none of the benefits.
Modular Monolith?
A modular monolith is a kind of compromise between microservices and a monolith. You build the app as a monolith, but you pay close attention to keeping the different modules separate and independent (this is good practice regardless).
This method retains the simplicity of the monolith, while still remaining flexible enough to slowly transition to microservices if/when the need for it becomes more pronounced.
Analysis
The companies that migrated to microservices tend to be very large (Netflix, Atlassian, Twitter, etc.). They have multiple teams of developers working on various parts of the application. Microservices are useful for them because the benefits at that scale are worth it for the extra complexity.
Most companies are not as big by comparison and that should prompt you to question whether microservices would be a good idea for your situation. As with anything else, it has benefits, but they’re not free.
Scaling
Being able to scale modules up individually can be nice, but there are often lots of opportunities to address performance in ways that don’t require a wholesale change in architecture. For example, using some insights from DataDog had a dramatic impact on performance for us in a very small amount of time and with very little cost.
It's a good idea to look for other opportunities like this that can make a big difference and cost much less.
Fault Tolerance
Being able to isolate issues to individual modules so that they don’t bring down the entire app can be nice, too. However, having good test coverage can help a lot more in terms of helping you identify where a problem is and how to fix it. It will also help prevent a lot of problems in that you can quickly verify that you're not breaking something before deploying new updates to the code.
Testing is also something you usually have to do regardless, and having a unified test suite in a monolithic app will be much easier to deal with than managing multiple different test suites across multiple different microservices.
Deployment
Deployments for a monolith are simpler than having to manage multiple different deployment pipelines for all of your different microservices. And if you do get a bug that crashes the system, having a simpler deployment process can help you get the fix out faster.
There's also the issue of how you'll be setting up the repos on GitHub. (You can check out this video for an overview of that whole problem.)
It may be that once you have the microservices deployment system set up, deploying to a single microservice could be simpler. But keep in mind that doing the work of setting up that microservices deployment system will likely be pretty complicated.
The Distributed Monolith
The danger of your microservices inadvertently turning into a distributed monolith is very real. Tight coupling across different modules can happen very easily if you're not careful. And it's not always obvious to figure out where one service ends and another begins.
If you commit to microservices from the start and come across this problem, you're stuck.
Recommended Solution
The Modular Monolith
Building an app as a modular monolith is something you should consider before diving into a microservices architecture. It lets you keep the simplicity of the monolith and gives you the flexibility to transition to microservices down the road.
And if you do decide to transition to microservices, you now have the option to do it more gradually, extracting out one module into a microservice and seeing how that goes. As opposed to having to manage 20 different microservices off the jump.