Monoliths - Good or Bad?
Over the past few months I've noticed some strong opinions on social media from people pushing back against the idea of microservices in favour of "monoliths". And it's not surprising. Pretty much every new architectural pattern (as well as programming framework) tends to over-promise and under-deliver.
What's more, even well-known advocates of microservices are quick to point out that they can introduce as many problems as they solve, and encourage you to "only use microservices if you absolutely need to".
In this (rather long and rambling) post I'd like to share some of my thoughts about what we actually mean by "monolith" and explore what the benefits as well as weaknesses of that approach are.
Of course I hope it goes without saying that even though I've devoted quite a lot of time over recent years to thinking about microservices and monoliths, I still don't consider myself to be an "expert" in either. My context and experience is probably quite different to yours, so I'd be happy to hear your perspective in the comments.
What is a Monolith?
The word "monolith" simply means a "large single upright block of stone", and so when it's used to describe a software architecture or codebase it implies that the application is (1) very large (I'd take that as at the very least 100K lines of code, and certainly 1 million or more would count), and (2) is not a "distributed" application made up of multiple services or microservices. Instead it runs as a single process.
Now obviously this is a rather loose definition, but I want to consider several typical characteristics of a "monolithic" architecture, and identify both the advantages and disadvantages of each.
One repo
First of all, in a monolith, all your source code is stored in one repo.
Advantages: The advantages are that there is just one thing to clone, and it can make it much easier to find things. If there is some logic to give customers a discount, I know that it is somewhere in the one repository.
Disadvantages: The disadvantages are that this can be a source of "cognitive overload". A new developer clones the repo and can be overwhelmed with the amount of code and not know where to focus. Another challenge can be that if many developers or teams are working on different features in parallel, then you can end up with vast numbers of branches and pull requests unrelated to your work. And if one developer makes a mistake that breaks the repo in some way, this impacts all developers, not just a single team.
One IDE
Second, with a monolith you typically load the entire codebase into one IDE (e.g. Visual Studio or VS Code).
Advantages: The advantages are that built-in IDE tooling can help you find everything. For example, I want to find all the callers of a particular method. It also becomes possible to debug much more easily than you can in a distributed system. Every line of code can be stepped through easily.
Disadvantages: The disadvantages are that many IDEs can start to exhibit serious performance issues on large codebases, with slow build times and maybe your corporate virus scanner slowing things even further. Another challenge is that you have to be extremely vigilant to maintain the modularity you've designed. It's all too easy when all the code is in a single IDE for one part of the codebase to directly reference something it shouldn't, resulting in a tightly coupled mess that takes a lot of effort to undo.
One executable running on one server
Third, a monolith is typically compiled into one executable which runs on one server.
Advantages: There are several advantages here as well. First of all, all communication between modules happens in-memory which is great for performance compared to frequent network calls that happen in a distributed application. You also have a much easier task when it comes to observability - there is only one server to keep an eye on and only one set of logs to look at to troubleshoot a problem.
Disadvantages: However, there are some big disadvantages to this approach. The most obvious being that your only option with a single server is to scale by "scaling up" to more and more powerful servers, which gets expensive and reaches a limit. So even with a "monolith" you're probably going to end up running multiple instances of your one app and dealing with at least some of the challenges of distributed systems.
Another big disadvantage of putting all your code into a single executable is that there might be some parts of your codebase that are unstable. Maybe you have to deal with obscure legacy file formats that might crash the process or hog huge amounts of memory. With the monolith approach, an error anywhere has potential to bring down the whole application.
One language and tech stack
Fourth, a monolith is written in one language and tech stack
Advantages: The key advantage here is that there is consistency and your whole can be effective because they all know that language and framework.
Disadvantages: However, this can be one of the most painful aspects of a monolith. Before long, the framework you originally used becomes out of date. I've lost count of the number of times I've needed to port a codebase from one framework or SDK to another over the course of my career, and monoliths can make this almost impossible, because you usually have to make the whole transition in one hit. It only takes one or two quirky legacy features that don't move across easily to block the whole migration.
My experience has been than monoliths almost invariably get stuck on legacy technology, while microservices are much more feasible to upgrade (or even completely rewrite - something that again is almost impossible with a monolith).
One Database
Fifth, it is typical in a monolith to store all your data in one database.
Advantages: There are several obvious advantages here. If it’s a relational like SQL server, then any conceivable query is possible - you can join across any table. It also means you've got a single place to focus on for optimization, resilience, backup and security concerns.
Disadvantages: What about disadvantages? Well not all data is equal. Some tables you mostly append to and rarely query, while other tables are rarely updated but frequently queried. Some entities are only ever looked up by their ID while others need very complicated sorting and filtering. Some data is business critical and needs to be backed up with expensive geo-redundancy whilst other data could housed in a much cheaper store.
Another challenge in a monolith is the concept of "ownership" of data - in a situation where everyone owns everything, developers often feel free to make schema modifications that suit their own needs but introduce performance issues for other features.
One deployment
Sixth, a monolith is deployed as one thing. When you upgrade, the entire application is replaced with the latest version.
Advantages: The big advantage here is simplicity. Only one thing gets deployed, and you don't need to think about the complexities of whether all the versions of different services are compatible with one another.
Disadvantages: However, this is another major driving factor in moving to microservices. Monolith deployments tend to be slow and manual. Because of this they are perceived as risky and so maybe only a few upgrades a year are permitted, rolling together many updates from many developers. It means that the delay from a feature being implemented to being live in production can end up being measured in months. You end up with ugly patterns like "code freezes" to try to stabilize the monolith before a release.
One of the major attractions of microservices is that they enable more frequent, smaller deployments, allowing completed work to go live much more rapidly. Of course, there's nothing inherent in a monolith that prevents rapid releases, but the reality is that it is rare for this to be achieved in practice with monoliths.
Another consideration is the need for "zero downtime" deployments. Where you have a monolith running as a single stateful process, and your upgrade rolls up many changes including database schema changes, achieving zero downtime becomes very challenging, and you often find with monoliths a window of downtime measured in several hours for an upgrade.
One team owning everything
The final characteristic of a monolith is that one large team share ownership of the whole thing. Even if you have 100 developers who are broken up into smaller teams of 5-10, nothing is out of bounds to them - anyone can change any part of the codebase.
Advantages: The advantage is that you are never blocked waiting for another team to do some work for you. If your feature requires changes to the frontend, backend, and database schema you can just go ahead and change it all.
Disadvantages: The disadvantage is that this can quickly result in the code becoming a tangled mess of competing architectural approaches and multiple abstractions for the same thing. The cognitive overhead of understanding the entire codebase is too large, and so developers often invent their own solutions to problems that fails to take into account the original architectural intent.
This of course does not necessarily need to be the case in a monolith - you can and should still modularise the codebase well, and ideally have experts or "owners" of each module who can code review changes to that part and guide developers to work with rather than against the existing design. But a "free for all" approach typically results in accumulation of technical debt.
Are microservices better?
Microservices are one way to address many of these disadvantages, but are not the only way. They also come with plenty of potential disadvantage of their own, which you need to be aware of and have a plan to deal with.
In my opinion, microservices are only attractive when one or more of the disadvantages listed above become so painful that you are willing to forego the advantages of monoliths. You have to make this decision for your own project based on your context - there is not one size fits all.
It seems to me that there is a lot more innovation happening in the industry aimed at making it easier to be successful with microservices than there is aimed at making it easier to be successful with monoliths. I think that's a shame. We need to work at this problem from both ends.
Do monoliths actually exist?
Finally, you might argue that my description of a monolith is not actually that likely to exist in practice. Pretty much any large-scale enterprise application is "distributed" to some extent.
For example, at the very least, the database is likely to be running as a separate process on a different server. You may well have a message broker, or cloud file storage involved. If you're following security best practices, you likely have a separate identity server of some sort.
And you might not be running a single instance of your monolith. It's not common to run multiple instances for scale, or be running different instances in different regions.
If you have any asynchronous message handlers, or scheduled tasks, it's also likely that they are running in a separate process to any web servers you are running.
So it's more of a sliding scale, with monoliths consisting of fewer distributed parts, while microservices might have many. But there is obviously a possibility of a middle ground where you have a monolith augmented with a few auxiliary microservices, or you have a small number of fairly large "microservices" that work together.
It's that middle ground where many of the systems I've worked on tend to sit. Hopefully in this situation you're benefitting from the advantages of both approaches rather than suffering the pain of both.