While the vast majority of software in usage today doesn’t use a microservice architecture, it has been hailed as the best way to build “cloud native” software for almost a decade now. The architectural style was pioneered by Netflix, who migrated from a monolithic architecture to microservices in 2010. Naturally, this inspired other companies to do the same. However, you should think carefully before adding a new microservice to your existing monolithic architecture.
Before we dive further into the subject, we have to take care of some definitions. First, what exactly is a microservice? In his book “Monolith to Microservices” Sam Newmann explains it like this:
Microservices are independently deployable services modeled around a business domain. They communicate with each other via networks, and as an architecture choice offer many options for solving the problems you may face. It follows that a microservice architecture is based on multiple collaborating microservices. They are a type of service-oriented architecture (SOA), albeit one that is opinionated about how service boundaries should be drawn, and that independent deployability is key.
The two most important parts of this definition are that microservices need to be independently deployable and that they communicate over the network. It is also interesting that Newmann doesn’t mention the size of the services in his definition. He explains later in the book that size is highly contextual and hence he doesn’t consider it overly important. In any case, using a microservice architecture means that you’re using a distributed system.
Newmann stresses that microservices can only deliver their benefits if they can be independently deployed. He defines independent deployability as follows:
Independent deployability is the idea that we can make a change to a microservice and deploy it into a production environment without having to utilize any other services. More importantly, it’s not just that we can do this; it’s that this is actually how you manage deployments in your system. It’s a discipline you practice for the bulk of your releases.
That’s very different to how deployment works in a monolith where all the components are deployed together at fixed intervals. Now, why is it such a big deal that microservices communicate over the network? Newmann explains it as follows:
Communication between computers over networks is not instantaneous (this apparently has something to do with physics). This means we have to worry about latencies—and specifically, latencies that far outstrip the latencies we see with local, in-process operations. Things get worse when we consider that these latencies will vary, which can make system behavior unpredictable. And we also have to address the fact that networks sometimes fail—packets get lost; network cables are disconnected. These challenges make activities that are relatively simple with a single-process monolith, like transactions, much more difficult. So difficult, in fact, that as your system grows in complexity, you will likely have to ditch transactions, and the safety they bring, in exchange for other sorts of techniques (which unfortunately have very different trade-offs).
This is another big change compared to monolithic architectures where communication usually happens between processes or threads.
Now that we have defined microservices, let’s talk about monoliths. Newmann correctly points out that monoliths are often distributed systems themselves just on a very small scale. Let’s take a 2010ish architecture as an example: We have a couple of Java web applications servers where all our code is deployed. These all talk to the same database and we have a load balancer and a caching service like Redis in the mix as well. While this is a distributed system, it is much easier to deal with the inherent difficulty of such systems on such a small scale. It is also important to keep in mind that not all monoliths are created equal: Some have so little internal structure that everything seems to be connected to everything while others are neatly divided into hundreds of different modules. In our example architecture, an unstructured monolith would just contain a single big jar/war-file which gets deployed on each application server while a structured monolith would consist of hundreds of such files which are deployed together. Such a structured monolith allows different teams to work in parallel as long as modules are properly defined. For many organizations such a structured monolith can work very well. However, unstructured monoliths can be very difficult to work with and are probably the main reason why monoliths have such a bad reputation. In an unstructured monolith it is often unclear who owns what part of the code and who makes decisions. As a result, it is very easy for developers to get in each other’s way. Also, the lack of ownership results in a lack of initiative as no one feels responsible.
The worst kind of monolith is the distributed monolith. Here, the system consists of numerous services, but for some reason all of them need to be deployed together. This architecture not only doesn’t deliver the advantages of microservices, it manages to combine the disadvantages of a distributed system with those of a monolith. Sadly, it is quite easy to accidently create a distributed monolith by adding microservices to your existing monolith. Which brings us to the main topic of this post. It is important to remember that software architectures aren’t inherently good or bad. There is no reason to change your architecture just because it isn’t fashionable anymore. Sam Newmann puts it like this:
Unfortunately, people have come to view the monolith as something to be avoided—as something that is inherently problematic. I’ve met multiple people for whom the term monolith is synonymous with legacy. This is a problem. A monolithic architecture is a choice, and a valid one at that. It may not be the right choice in all circumstances, any more than microservices are—but it’s a choice nonetheless.
Before deciding to either move existing functionality into a microservice or to build new functionality in a microservice instead of in the monolith you need to make sure that it is truly necessary. You should only use a microservice if it is absolutely not feasible to have this functionality in the monolith. Each additional microservice makes deployment more complicated as you now have an additional component which you need to deploy in all environments. If you add too many microservices, you undermine the easy deployment process which is a key advantage of a monolithic architecture. In addition, every additional microservice makes it more difficult to achieve system wide transactions, reduces performance by replacing method calls with network hops and makes log consumption more difficult. Code reuse is easy in a structured monolith (and maybe too easy in an unstructured one), but difficult to realize when using a microservice based architecture. Adding microservices degrades a monolith by diminishing its benefits. Furthermore, microservices can only provide benefits if they can be independently deployed. If this won’t be the case, then it is probably a bad idea to add a microservice to your architecture. After all, if it needs to be deployed together with a new version of the monolith, it can be part of the monolith itself.
So, if adding microservices to a monolithic architecture degrades the monolith when should we do it nevertheless? Well, the monolith can only work properly if it is structured. Otherwise, it can slow you down so much that you want to get rid of it completely. In that scenario, replacing the monolith iteratively with microservices can be a good idea. However, there is an alternative: Adding structure to the monolith. This is a tedious process, but it has two big advantages: It can be done without a big rewrite and it enables us to keep the advantages of the monolith. If you decide to add structure to your monolith, the first step is to stop the decline.
Both moving to microservices and improving the monolith are equally viable options. One reason to use a microservice instead of extending the monolith is that you need to be able to scale that microservice horizontally. Monoliths are difficult to scale and hence work best if your load is static. If you have peak scenarios where the load climbs dramatically, you have to overprovide for the normal usage when using a monolith. A microservice can scale horizontally as you can quickly spawn new instances of it when necessary. Here, a microservice is providing value by doing something which the monolith can’t. If you have this kind of scaling problem in many parts of your system, then switching to a pure microservice architecture might be a good idea. If only small parts of your system need to scale horizontally, you can use microservices for these parts and keep most of the remaining code in the monolith.
A similar scenario is that you want to use a different piece of technology in the microservice which cannot be used in the monolith. One of the most cited advantages of microservices is that every microservice can be written using the most suitable programming language and frameworks. In my opinion, that is a poor reason to use a microservice as the differences between languages aren’t that great. Using the newest and coolest tech is very appealing to a lot of developers, but it is a poor base for architectural decisions.
If you currently have a reasonably structured monolith, you should think twice before adding any microservices to the mix as they will degrade the monolith. If your monolith is unstructured, you can either slowly switch to microservices or iteratively add structure to the monolith. Both options are viable. If you liked this blog post, please share it with someone. You can also follow me on Twitter/X.