I recently read a post called The False Dichotomy of Monoliths and Microservices by Jimmy Bogard, which I absolutely loved. While reading through it I noticed he touched on a witticism I use, which is to refer to most microservices implementations as “distributed monoliths”. I very briefly thought that maybe I had coined the term, but Google quickly disabused me of that ridiculous notion. I say “ridiculous” because I should have known that the phrase would have been previously used, since it so perfectly describes what most greenfield microservices implementations turn into.
Before we get into the details of that, I have to lay out a confession: I’m a big fan of the monolith. It has received so much bad press in recent years due to the microservices craze. But I think the real root of the problem is that developers work with applications that slowly accrete more and more functionality (often without tests) over a course of many years and those applications become very brittle and difficult to change. I’m not going to deny it, this is a huge problem! This is one of the core problems that Service Oriented Architecture and now Microservices (are Microservices a subset of SOA?) are designed to solve.
I’m not building microservices?
Before answering that question, let’s define what most folks would consider the opposite of microservices… a monolith. One of the challenges these days is finding a good definition of a monolith, because they are most often defined in the context of implementing SOA or Microservices. I think that the Wikipedia definition is actually pretty good though:
A software system is called “monolithic” if it has a monolithic architecture, in which functionally distinguishable aspects (for example data input and output, data processing, error handling, and the user interface) are all interwoven, rather than containing architecturally separate components.
The basic idea is that instead of being broken out into separate architectural components, things are instead intertwined. To Jimmy’s point from his post above, there is nothing in this definition about the fact that these pieces are, or are not, physically distributed from each other. We like to think of monoliths as a large single codebase, running on a single system, but that really isn’t the case. The core tenet of a monolith is that the system functionality is intertwined.
So, if we follow that course of logic, then what would a distributed monolith be? Well, it would be a set of physically distributed services that are intertwined. Sound familiar? I hope for your benefit, it isn’t too familiar.
You see, a distributed monolith is the worst of all worlds. You’ve taken the relative simplicity of a single monolithic codebase, and you’ve traded it in for a deeply intertwined distributed system. So, are you building microservices? Take a look at a few of these symptoms, and decide for yourself:
- A change to one microservice often requires changes to other microservices
- Deploying one microservice requires other microservices to be deployed at the same time
- Your microservices are overly chatty
- The same developers work across a large number of microservices
- Many of your microservices share a datastore
- Your microservices share a lot of the same code or models
Now, I’m not saying that if your microservices implementation checks one of these boxes that you have a problem… there are always exceptions. But for the most part, if you’re nodding your head at a number of the points above, you might not be working with microservices.
How do I get to microservices?
I’m a big fan of baby steps. And when it comes to microservices my thoughts are no different. For many greenfield systems I would almost always recommend starting off with a monolith. Monoliths are easy, and you can logically separate your system where it makes sense within a single codebase. But many systems have a lot of concepts that are inherently tightly bound, which are difficult to separate cleanly until you have a deep knowledge of the problem domain. Sometimes you realize that those concepts can never be separated!
Stefan Tilkov has a thoughtful essay arguing against the premise of starting with a monolith when you suspect you’ll need microservices. But he still acknowledges the difficulty of defining boundaries in a greenfield system: “I agree you should know the domain you’re building a system for very well before trying to partition it, though: In my view, the ideal scenario [for starting with microservices] is one where you’re building a second version of an existing system.”
Designing service boundaries isn’t easy, even with knowledge of a system, and getting them wrong is very costly. Starting out with a monolith allows you to figure out where the natural boundaries are in your system, so you can design your service boundaries in an informed way.
Avoiding the Distributed Monolith
I believe that many of the problems that people run into with small and medium sized monoliths can be alleviated by having a good automated test suite and automated deployments. Having a single, non-distributed codebase can be a huge advantage when starting out with a new system. It allows you to more easily reason about your code, allows you to more easily test your code, and it allows you to move quickly and change quickly without having to worry about orchestration between services, distributed monitoring, keeping your services in sync, eventual consistency, all of the things you’ll run into with microservices. Things that might not seem like huge challenges at first glance, but in reality are huge hurdles to overcome.
If you start off with a monolith, your goal should be to keep it from growing into a monstrosity. The key to doing this is to simply listen to your application! I know, this is easier said than done, but as your monolith grows, keep asking yourself a few questions:
- Is there anything that is scaling at a different rate than the rest of the system?
- Is there anything that feels “tacked-on” to the outside of the system?
- Is there anything changing much faster than the rest of the system?
- Is there a part of the system that requires more frequent deploys than the rest of the system?
- Is there a part of the system that a single person, or small team, operates independently inside of?
- Is there a subset of tables in your datastore that isn’t connected to the rest of the system?
As the system size and developer count gets larger you’ll find that splitting out services will become commonplace. You hear about companies like Netflix or Uber that have hundreds of microservices in their ecosystems. These companies are at the extreme end of the scale. They have thousands of developers and are often deploying thousands of times per day. They need tons of fine-grained services in order to make any of this sane and manageable. But what they might think of as a fine-grained service is probably a medium sized application to a much smaller company. That is something that is important to keep in mind, the scale of some of the systems that are successfully deploying large numbers of microservices.
When you move significantly down from that extreme, you will start to find that leaning more and more towards having a handful of medium sized monoliths, surrounded by an ecosystem of services is what make more sense. When you get down to the other extreme end of the scale, where you have less than five developers working on an entire system, you’ll find that there are huge advantages to sticking with a monolith and potentially pulling out a service or two — if it really makes sense.
If there is anything you take away from this post, I hope it is that microservices are useful, but shouldn’t always be your default architecture of choice. Splitting out services makes some things easier and other things harder, so depending on your system and organization you’ll want to maybe think through things before jumping in head first.
To sum it up, I’ll leave it to Martin Fowler: “don’t even consider microservices unless you have a system that’s too complex to manage as a monolith.”