Microservices: when not to use them…Published on: Author: Marcel Otto Category: IT development and operations
Microservices are a hot topic, one of the buzzwords of the last few years. But are you old-fashioned for not using microservices? Should every project use them? When, 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 by the pros and cons of its alternative: the monolithic architecture. Then I will try to find a way to have the best of both worlds. Lastly, I will discuss the characteristics of when 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 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 certain languages. 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 a lot of time getting the library to work in your language, or searching for a similar library.
- Independent life cycle
Independent development and deployability are 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, and probably 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 is 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, often 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
Making an informed decision about whether to use microservices or not doesn’t just depend 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 choose.
- Simple code sharing
No shared libraries (or 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 and more RAM are the solution 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 a long time to roll back in case of failure.
Of course, there are more cons to a monolith. But a lot of the downsides are a result of bad (or inconsistent) coding practices, which are bad in any system. It’s 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 to 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 very chatty services can be considered a smell. Consider that perhaps they belong in the same service.
- The absence of database safeties like transactions and foreign key constraints can be minimized by picking the right boundaries for your services. When you can’t, there are a lot of solutions to handle transactions and keep your data consistent. Think about retries or the Memento Design Pattern to be able to ‘undo’ changes.
As mentioned above, it’s possible to partly circumvent or downplay the downsides of microservices. It is also possible to bring benefits of microservices to monolithic applications. One of these benefits is certainly loose coupling. While the common view of a monolith is that it conforms to the Big Ball of Mud antipattern, it certainly doesn't have to be that way!
Using the Dependency Inversion principle as a guide to keep the coupling of modules to a minimum and creating a modular-monolith (Simon Brown, 2016), will also keep “unknown” side effects in check.
Java 9 modules are a good way to let the compiler enforce this low coupling. This only works when you make your module API interface “module public”, and your other classes package private public.
This strategy of modularity not only has the benefit of loose coupling, it also leaves the door open for “easy” extraction of functionality in separate services.
WARNING: Modules should be vertical slices of your application, if you want to split them later. Horizontal slices (like a 3-tier application) do not have some of these 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! You need to 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 yet which modules will be very chatty? Don’t know which parts of your application need to be scalable? Don’t go microservices… yet!
Did you notice how I used the word “yet”? Simply put, this means don’t go full-on microservices when you don’t know what you are doing. There could be pitfalls on the 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 doesn’t have any benefit now, don't put a lot of effort into 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 tests will teach you a great deal about the current functionality, and certainly help you keep this functionality while refactoring.
The Strangler pattern is a good tool to replace parts 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.
- Make services that handle a certain part of the 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 invaluable. Do research! Read blogs and books from people that are big fans, but also from people that are skeptical.