Microservices: when not to use them…Published on: Author: Marcel Otto Category: IT development and operations
Microservices are hot topic, one of the buzzwords in the last few years. But are you old-fashioned for not using microservices? Should every project use them? When to, and when not to use them? This blog will shed some light on the pros and cons of this much discussed topic.
First, we will take a look at the key aspects of microservices, followed with the pros and cons of its alternative, the monolithic architecture. From this I will try to derive a way to have the best of both worlds. Then the time has come to further discuss the characteristics where microservices are probably not your best option.
Microservice architecture: an overview
A microservice application is a set of small, independently deployable services. It is an implementation of Service Oriented Architecture (SOA), where (usually) every stateful service has its own datasource. Some of the key benefits of this architecture are:
- Possibility for horizontal scaling
When one part of an application has to scale more than other parts. Especially stateless microservices are easy to duplicate. Docker Swarm and Kubernetes are useful tools here.
- Loose coupling
Microservices enforce modularity. Coupling only exists on the external (REST or RPC) API. This makes internal changes basically ‘free’ and switching between or replacing service implementations very easy.
- Freedom in technology
Some tools and framework only work well in a certain language. Data science and IoT tools for example are mostly written in Python and can be hard to include in Java projects. With microservices you can put the Python part in its own microservice instead of spending time getting the library to work in your language or searching for a similar library.
- Independent life cycle
Independent development and deployability is very useful when working with separate (functional) teams. You can also increase the uptime of part (or all) of the functionality. This is a tricky subject though, that deserves its own blog.
But nothing comes for free! A microservice architecture has some drawbacks:
- Extensive logging and monitoring
In distributed systems logging and monitoring are essential. Finding bugs or searching for performance issues will be very difficult without them.
- Slow calls between services
Compared to local calls, network calls are relatively slow and brittle. Networks can, and will fail (Newman, 2015)
- Lots of moving parts
With a single application, a lot of times the compiler, simple unit or integration test will warn you in case of breaking changes. In distributed systems, more effort is required. You will have to version your endpoints to stay backwards compatible and write ‘multi microservice’ tests to detect accidental breaking changes.
- Lack of database safeties
Transactions, foreign keys, and single query joins (Newman, 2015). Extra effort is required to keep data consistent and have transactions. Depending on a single DB is not possible if tables live within different microservices.
Another important thing to keep in mind in distributed systems is the PACELC-theorem. This theorem states that there is a tradeoff between consistency and latency in high availability systems. It will hold no matter what technology or tools you use. Simple caching for instance will trade consistency for lower latency.
The monolith: an overview
To make an informed decision about whether to use microservices or not, depends not only on the benefits and drawbacks of microservices of course. You have to know what your alternatives are, in this case, by building microservices you risk giving up the benefits of a monolithic application.
The benefits of a monolithic application are often the opposite of that of microservices:
- Fast calls
Every call is a local call.
- Single build and deploy
Compared to microservices, the building and deploying process is simple.
- Few moving parts
Operationally there are less moving parts to consider. Of course the amount and complexity of business logic remains the same whatever architecture you chose.
- Simple code sharing
No shared libraries (other solutions) needed here. This is especially useful with crosscutting concerns like logging and monitoring. You only have to set it up once.
- Only vertically scalable
If one part of your application needs a performance boost. Your whole application needs to scale. Bigger servers, more RAM are solutions here.
- Impactful deployments
While the deployment process is simple. The deploy itself has a higher impact. Scary big bang rollouts, take a long time, and take a long time to roll back in case of failure.
Of course there are plenty more cons to a monolith. A lot of those downsides are a result of bad (or inconsistent) coding practices though, which are bad in any system. It must be sad that things like high coupling and unexpected side effects can hide better in bigger systems, than in small services.
Can you have your cake and eat it too?
In theory? No! In practice it is perfectly possible to mitigate some of the downsides of a microservice architecture, or keep the benefits of a monolith while splitting. Splitting your application the right way is key here!
- Relatively slow communication between services is not as much of a problem if your services are not that chatty in the first place. Thinking about the communication between services beforehand can really pay off here. Having such chatty services can be considered a smell. Maybe if they are that, perhaps they belong in the same service.
- The absence of database safeties like transactions and foreign key constraints can be minimized when picking the right boundaries for your services. In cases where you can’t, there are lots of solutions to handle transactions and keeping your data consistent. Think about retries or the Memento Design Pattern to be able to ‘undo’ changes.
As mentioned above, it is possible to partly circumvent/downplay the down sides of microservices. It is also possible to bring some benefits of microservices to monolithic applications. One of those is certainly loose coupling. While the common view of a monolith is that it conforms to the Big Ball of Mud antipattern, it certainly does not have to be that way!
Using the Dependency Inversion principle as a guide to keep coupling of modules to a minimum and create a modular-monolith (Simon Brown, 2016). This also keeps ‘unknown’ side effects in check.
Java 9 modules are a good way to let the compiler enforce this low coupling. This works by only making your module API interface ‘module public’ and your other classes package private public.
This strategy of modularity not only gives the benefits of loose coupling, it also leaves the door open for ‘easy’ extraction of functionality in seperate services.
WARNING: Modules should be vertical slices of your application to be able to split them later. Horizontal slices (like a 3-tier application) does not have some of those benefits.
When not to use microservices you ask?
The simple answer would be: any time the cons of microservices really hurt and/or the benefits are marginal.
Don’t want to invest in (more) extensive logging and monitoring? Don’t go for microservices! Be prepared for unfindable bugs or performance issues! They will haunt you! Don’t have a properly automated CI pipeline yet? Your (bi-)weekly deployments will cost you!
Don’t know yet where your bounded context boundaries will lie? Don’t know what modules will be very chatty? Don’t you know what parts of your application needs to be scalable yet? Don’t go microservices…...yet!
Did you notice how many times I used the word 'yet' just now? Basically this means not to go full on microservices when you don’t know what you are doing. There could be bear traps on your way, that will grind your development to a halt.
Just like Robert C. Martin states in his book Clean Architecture (Robert C. Martin, 2017), good architecture allows you to defer critical decisions. If you start with a properly modularized monolith, splitting later will be much easier.
Final thoughts and recommendations
A good concept to think about when designing a microservice system is YAGNI. When splitting into microservices does not have benefits now, don't put effort in splitting the application right away. Those benefits may never come.
Splitting the big ball of mud
The first phase of splitting your ball of mud is testing. Writing ‘black box’ integration test will teach you a great deal about the current functionality and certainly help you to keep this functionality while refactoring.
The Strangler pattern is a good tool to replace part of functionality one at a time. The idea is very simple:
- Put a proxy between the consumer (like the UI) and your ball of mud.
- Then make services that handle a certain part of existing functionality.
- Let the proxy redirect the consumer to the new services instead of the monolith.
- Once all functionality has been split. You can throw away your monolith!
Awareness is half of the battle
Being aware of the pros and cons of microservices is priceless. Do research! Read blogs and books from people that are big fans, but also from people that are skeptical.