How To Fail at Micro-Services
Learn about anti-patterns and bad smells with micro-services
Photo by Kenny Eliason on Unsplash
Doing micro-services the wrong way can be a death sentence for your project. If your architecture resonates with some elements of today’s list, it might be a good time to stop what you’re doing and reflect on it.
First, we need to define the basic properties of micro-services. Amazing resources like Building Microservices and Microservice Architecture tell us they are supposed to be:
Small in size
Messaging enabled
Bounded by contexts
Autonomously developed
Independently deployable
Decentralized
Built and released with automated processes
Loosely coupled
Focused on one thing
If you built your back-end with micro-services in mind, but don’t respect some of those properties, it’s an obvious sign you made a mistake.
But sometimes, wrong architecture choices sneak up on you. You end up with something close to micro-services but don’t respect some of their rules.
Distributed monolith
The most common is to build a distributed monolith instead of micro-services. This is a complete subject in itself, I highly recommend you read more about it.
Shared database
Let’s get this straight: it’s impossible to create independent and loosely coupled services with a shared database.
You will quickly realize how shared ownership of a database is a nightmare and has a negative impact on development and maintenance. It doesn’t integrate too well with ORMs, and I’m not even talking about deployment.
Yes, in a monolith you can have a single database, which makes things easy. With micro-services, you don’t have a choice but to make it more complex by separating databases for each service.
It’s one of the trade-offs with micro-services, it’s an added complexity that allows you to take advantage of micro-services.
Lack of automation
Micro-services definitely require more effort to develop, deploy, and maintain. That’s why one of the cornerstones of your system should be automation.
I guess you could try to create a few micro-services without automation, but on a real project that’s simply unrealistic. Mainly, testing and deployment quickly become impossible without a completely automated CI/CD system.
Shared business logic
Sharing business logic goes against the properties expected from micro-services, as it makes your services highly coupled.
And, in practice, you will also realize it has fewer benefits than expected. Using a shared package for business logic forces you to update the package itself, before updating your services.
It makes development longer and causes context-switching for developers, without bringing any real value.
Starting with micro-services
In recent times, micro-services became trendy. While they solve several specific challenges, they’re not a silver bullet.
In practice, they should only be used when you already have a monolith, but have challenges micro-services solve.
Let’s say you started your back-end application, day one, as micro-services. There is a higher probability you did so because you wanted to use micro-services than to solve a real challenge.
Lack of tests
Micro-services should be tested as much, if not more, compared to a monolith.
They should be tested in isolation, but that’s not all. If your services communicate (usually through events), it means they agree on a contract (implicitly or explicitly).
This contract should definitely be a part of the scope of your tests.
Not independently deployable
If, for some reason, you are unable to deploy a new version of a service in autonomy, it’s an obvious sign your services are highly coupled.
You shouldn’t need the other services at any point in the development and deployment process of a service. In Hands-on Microservices with Kotlin, we learn:
Having the ability to deliver constantly is one of the advantages of the microservices architecture; any constraints should be removed, as much as we remove bugs from our applications.
We should take care of deployments from the beginning of the design of our microservices and architecture; finding a constraint on this area at late stages could have a big impact on the overall application.
Synchronous communication between services
Using synchronous (request-response) communication between services breaks the independent & loosely coupled properties.
If you use synchronous communication between services, you are actually building a distributed monolith. After reading about distributed monolith, you should know it’s the exact opposite of what we want.
It doesn’t bring the added value of micro-services but also loses the simplicity that comes with a monolith.
Moreover, with micro-services, we need to guarantee messages get delivered. With request-response communication, when a process extends to more than one service, it’s simply impossible.
Need for other services to work
Maybe the most important, but still the most often ignored.
One of the downsides of synchronous communication between services is the need for multiple services to work at the same time.
In theory, one service should be able to work perfectly fine even if all other services are down. Obviously, the system itself won’t work as expected, but services in isolation should.
This is generally made possible by:
Limiting relationships between services
Using deduplication
Communicating through events.
If you are looking for an example projects with micro-services, you might be interested in a tic-tac-toe game and its score.
Otherwise, if you are looking for a more in-depth, and practical course on micro-services: Good news, I teach micro-services in a complete guide.