Microservices are very much in vogue when it comes to web software architecture. There are a large number of high-profile practitioners and proponents of the Microservice architecture.
Facebook, Uber, Groupon, Klarna, Amazon, Netflix, eBay, Comcast, and more.
You are probably not these companies. Which is to say, your team probably doesn’t look anything like these companies’ teams. You probably aren’t facing the same problems they are.
If you are (but you probably aren’t), or are on track to be in the next year or two, stop reading. You might need microservices.
But here’s the thing. If microservices do make sense for you, resisting them, really asking yourself why you need them, will help shape them for your specific use case.
Microservices’ Straw Man
There are a number of purported benefits of microservices. The problem isn’t so much that microservices don’t provide these benefits. Rather they aren’t addressed exclusively by microservices.
Most, if not all, of these things can be achieved, to some extent, with a monolith.
When someone lays out the pros of a microservice architecture, the implication is that in order to solve these problems, you must adopt a microservice model.
The reality is that microservices allow you to buy into an additional layer and degree of flexibility to address these concerns.
The key words are BUY INTO.
Microservices aren’t free. They are notoriously expensive to build and maintain.
If you need that additional flexibility? Great. Maybe that additional cost, on balance, is going to pay off. Maybe microservices are a good fit. Maybe you should consider them seriously.
If you don’t? Well, you just over-engineered your stack and have seriously hampered your team's ability to deliver value to customers.
Let’s look at some of the most commonly put forward benefits of microservices and consider how we can mitigate against these concerns with a monolith.
Scalability
Running microservices means each function of your application is running on its own resources that can be scaled independently of each other. Of course, this is going to give you a high level of control over exactly how many and precisely which type of resources are assigned to each function.
But do you really need that level of control?
Do the different functions of your application experience different levels of load?
Do they tend to scale at different rates?
Do they have different requirements in terms of CPU, memory, storage, and GPU?
Throw Boxes at It
For a lot of teams, it is going to be cheaper to cover the disparities in these resourcing concerns by simply increasing the size or number of boxes available across the board. IE. Most of the time, it is more cost-effective to not optimize infrastructure to within an inch of its life.
Fix Your Monolith
Odds are it is easier to resolve performance issues and bottlenecks in your monolith than it is to transition to a new architecture pattern. The details of this are stack specific, but you shouldn’t have to go too far to find ideas on how to tighten up your app.
Route Traffic to Independently Scalable Clusters
If you have more than a single server, you are going to be running some kind of load balancer in front of it. You can use the configuration of this load balancer to route traffic to independently scalable clusters of your app instance.
You can also pull asynchronous tasks into background jobs with independently scalable queues. Ensure that you have enough queues to give you the granularity of control over the number of boxes necessary to keep your queues down and your infrastructure costs reasonable.
Fault Isolation
The ability for a single feature of your application to go down without taking the rest down with it is a big bonus of a properly designed microservices architecture.
Route Traffic to Isolated Clusters
The idea of routing different kinds of traffic to clusters for scale can also provide a measure of protection against faults.
Think about where most of your historical faults come from. This will give you an indication of how to best route your traffic. If you struggle with “noisy neighbors”, you might want to route based on account ID. If you have a brittle but secondary function that has a habit of bringing everything down, maybe route its endpoint to its own cluster of app instances. If you have problematic background jobs, stick them in their own queue that doesn’t impact the other jobs.
Automated Testing
Prevention is better than a cure. If you can prevent or at least lower the number of faults, you have to worry a lot less about isolating them.
Nurturing a healthy test culture to ensure confidence in the quality of what you are shipping is key. Not only in its ability to reliably deliver the required functionality. But also in its ability to do so under load in production.
Programming Language and Technology Agnostic
There aren’t many options for introducing new languages and technologies without them being separate services.
In most cases, this is more of a feature than a bug. Giving engineers too many options when it comes to choosing languages and technologies can lead to a fragmented and overly complex tech stack.
Lean into the simplicity and alignment that a monolith brings.
If you do have domain-specific requirements for specialist languages targeting specific features, you may want to consider separate services. You will need to weigh the benefit that any additional technology brings with the added cost of maintaining it.
Data Security
You think securing a monolith is difficult? Microservices are only going to make this job more complex. By increasing the complexity of your stack, you are increasing the surface area of attack.
Secure Your Monolith
It is true that isolating functions into different services allows you to apply different levels of security and diligence to each of them. However, consider whether this level of control is necessary.
Is it easier to secure your entire monolith to the highest level necessary?
Do you even have different security requirements for your data?
Team Autonomy
I’m a big fan of autonomous cross-functional teams. And I’m a little confused by where the idea of having to introduce network boundaries to make this happen came from.
Get Matthew Spence’s stories in your inbox
Join Medium for free to get updates from this writer.
Giving each team ownership of specific isolated systems might seem like a way to improve team autonomy, but in reality, it can work against it.
Let’s say my team needs a change in a piece of functionality that another team owns. With a microservices architecture, I’m probably going to have to draw on their knowledge and experience of that service to make changes to it. I might even have to wait for them to do it. If the feature is in a monolith, there is a good chance I’m already familiar with the code or at least its conventions.
To what degree teams can be autonomous is a function of how well modularized and how consistent the system is. Neither a monolith nor microservices will guarantee or doom you in either of these regards. However, microservices will force your system to be modular, and a monolith will tend to encourage more consistency.
Modularize Your Monolith
Microservices, by their nature, will force you to modularize your system. Monoliths don’t really provide much help here (your monolith framework of choice might, though), but they certainly don’t get in the way of doing it yourself either.
Loosely coupled modularized code with limited concerns is a good idea regardless of whether you happen to have the added complexity of network boundaries between those concerns.
Microservices Don’t Ensure Good Modularization
Although microservices enforce modularization, there is no guarantee it is good modularization. Microservices can easily become a tightly coupled “distributed monolith” if the design isn’t fully considered.
If you are unable to successfully modularize a monolith, you are going to struggle to structure a successful microservices architecture.
Microservice force modularization, but they make doing it well harder.
Independently Deployable
There are a number of changes you might make where being able to independently deploy services is a benefit. But for your bread and butter. Your day-to-day changes. This can become a sticky bottleneck.
The requirement to orchestrate a number of deployments across separate services is going to make releasing features quickly harder and more complex.
Breakdown Complex Changes
With a monolith, it is still possible to break complex, risky changes into separate deployments. A common example is making migrations forward and backward compatible with their own PRs and their own deployments.
This obviously adds a degree of complexity to shipping functionality in the same way microservices do. You wouldn’t want to use it frivolously. But it does provide some of the benefits of microservices without being forced to utilize it for every change.
Dependency Management
Microservices allow you to have separate dependencies for each service, but do you really need them?
It’s hard enough to manage dependencies in a large monolith. Splitting it into several smaller lists simplifies managing each individual list but can complicate doing it for the system as a whole.
With a monolith, sooner or later, you are going to run into “dependency hell” and have a conflict between two dependencies. Microservices aren’t going to ensure you avoid this, but they should reduce the likelihood of problems.
Stay on Top of Dependency Updates
Keeping your dependencies up to date is obviously desirable. But in the real world, usually, something that is allowed to slip.
Staying at the bleeding edge of available versions might actually compound this issue with one dependency moving forward faster than other related dependencies. But “staying on top” doesn’t have to mean updating to the very latest version in every case possible.
Simpler, Easier to Understand Code
This benefit is at best disingenuous, at worse, a bald-faced lie.
Each service is simpler and easier to understand. Sure. The system as a whole is far more complex and harder to understand. You haven’t removed the complexity; you’ve increased it and then transplanted it somewhere else.
Modularize your Monolith
We don’t need to introduce network boundaries and isolated processes in order to make our code easier for engineers to reason with.
A monolith broken down into modules with clearly defined and limited concerns is going to be just as easy, if not easier, to understand than a system broken down into separate services.
But We’re Feeling the Pain of our Monolith
If you are experiencing problems with your monolith, it’s probably because there are problems with your monolith, not because it is a monolith.
It’s possible your monolith is a shining beacon of code quality, tooling, and modularization. If this is you and you are still experiencing pain, it might be time to start thinking about microservices. But this probably isn’t you. You probably just need to improve your monolith.
Building software is hard. Organizing large complex systems with lots of moving parts that evolve over time is very hard.
I’ll put my hands up. I’ve built monoliths that are a mess.
I’m not here to judge anyone.
But if you are assuming that the problems that you are facing with your monolith will be magically resolved, or even simplified, with microservices, you are in for a world of pain.
Should I Consider Microservices?
The choice between monolith and microservices is often presented as two mutually exclusive modes of thought. Old school vs. new school. Right or wrong. One or the other.
The truth is they are both valid approaches with different trade-offs. The correct choice is highly context-specific and must include a broad range of considerations.
The choice itself is a false dichotomy and, in certain circumstances, should be made on a feature-by-feature basis rather than a single approach for an entire organization’s engineering team.
Should you consider microservices?
As is often the case, it depends. You might genuinely benefit from a microservices architecture.
There are certainly situations where they can pay their dues, but if you are a small to medium-sized team or an early-stage project:
No, you probably don’t need microservices.
Don’t Believe Me? Ask Around
Ask around the industry, and you’ll find an endless list of cautionary tales about microservices and the problems they’ve caused teams.
Segment is one well-documented and high-profile example of a team that has u-turned on microservices.
The same applies to microservices as monoliths. Just because they had problems in their execution doesn’t mean the core premise is fundamentally flawed.
However, if you are going to adopt a microservices approach, you need to go in with your eyes wide open. Accept the tradeoffs, and be prepared for the additional resources it is going to require to pull off successfully.
My View
The monolith should remain the default choice for new, small, and medium-sized engineering teams. Microservices are still an option, but you should have compelling context-specific reasons to justify their use.
For medium to large teams, it should be considered but with a great deal of apprehension and understanding of the tradeoffs.