Why Microservices are NOT a Good Idea
In a world of grid computing, event driven architecture, and ChatGPT, who doesn't love the word "microservice". It fits so well with the other buzz words like "distributed systems", "fault tolerance" and "resiliency" and can score quick points on most tech interviews.
While it's true that hating on the dreaded "monolith" will score easy points on your next tech screen, it's important to understand the pitfalls of taking such a fragmented approach to designing your system.
Yes, hating on microservices can be an uphill battle. Most organizations have embraced the idea of micorservice based architecture and it can be hard to highlight just how much the cons outweigh the pros. When you see the benefits it brings companies including maintainability, organization, and efficiency, it can be really hard to hate on microservices...
But I've always liked a challenge. And I've spent over 10 years working at organizations large and small who get caught up in the microservice craze. Not only have I seen microservices significantly slow teams down, I've seen them result in some pretty messy production issues.
So let's begin highlighting some of these cons. Let's explore why microservices can be a bad thing. Let's make you a little less afraid to start with a monolith for your next big idea.
But first, let's play devils advocate in exploring the GOOD in the popular world of microservices.
Why are microservices good?
The dreaded monolith
Microservices help escape the dreaded monolith: A single process that runs the entire solution. If you think about a traditional web stack, a monolith approach would involve including the database logic, service layer logic, and UI all in the same executable artifact.
While this can be an easy solution for managing the code base and deployment of your system, the monolith introduces some serious pain points. An issue with any part of the application can bring the whole thing crashing down. Say there is an issue with the UI not displaying information correctly. In order to fix this, you need to redeploy the ENTIRE solution.
Additionally, a single code base becomes increasingly harder to maintain over time. The more features you add, the more time it takes to debug issues. The more complex the code base, the harder it is for a new developer to jump in.
Scalability presents another complication. Increasing the performance of a monolith solution is largely limited to vertical scaling aka adding more CPU or memory to the whole process. If your applications' database queries require more computing power but your service layer is handling requests just fine, you still have to throw more computing resources at the WHOLE solution (not just the part that needs more resources).
Microservices to the rescue
Such pitfalls with the monolith have been addressed over the past decade. Advancements in containerization (think Docker) and cloud computing have made it possible to deploy isolated units of functionality that play well with one another. Orchestration frameworks like Kubernetes (k8) have made it possible to configure the desired state of a network of services working together to achieve an overall solution.
Which brings us to microservices: independent units of functionality that are independently deployable and work together to create a wholistic solution.
Instead of having a monolith to host your web stack, you now can break your solution down into separate services. The UI can run in its own container separate from the API layer. The API layer code base can live separately from the database logic.
With microservices, things can exist independently. The solution is less tightly coupled. Development teams have more flexibility in the languages and technologies they use for different services. If the database team loves Python they can build the database service with Python while the API service layer team has a field day with Java.
New team members can quickly jump into more manageable code bases. When problems arise, individual teams can fix them without coordinating across the whole organization.
Things that require more scalability can get the resources they need without souping up the entire system.
WHATS NOT TO LOVE!?
What are microservices pros and cons?
Pros
Scalability
Microservices facilitate scalability. Services exist in their own "contanerized" environment. Resources can be allocated and added to services as needed. Services don't compete with other processes for resources like a monolith.
Independent Deployment
Microservices can be independently deployed. Rolling out changes to service A doesn't require also deploying service B. A service can be updated and released without affecting other services in the system.
Flexibility
Micorservices are flexible. Different services can be written in different languages and still work together. The nature of microservices makes them smaller and more managable. This gives development teams a lot of flexibility in how they choose to work as part of a larger organization.
Maintainability
Microservices are easy to maintain. They represent smaller units of functionality and result in smaller code bases. Less code makes the service easier to read and jump into as a new developer.
Cons
Coordination
Coordinating communication between microservices can get messy. While microservices are (ideally) loosely coupled, a relationship to other services must exist to achieve a wholistic solution.
Maintainability
Yes, a pro can also be a con. While microservices are typically smaller components that are easier to maintain, they can be difficult to manage collectively. Managing a change or releases across different services can become complicated fast.
Networking
A collection of microservices can only work well together if they can communicate. This typically requires network calls to talk across these different services. Although services can be deployed in the same physical network, this communication introduces a lot of network activity that can introduce latency and slow the system down.
Resiliency
Managing the resiliency of your system can get extremely complicated when taking a microservice approach. What happens when certain calls work but other calls fail? How do you handle a failed message from one microservice to another? This can get extremely tricky and hard to manage and introduces a lot of complexity to the soution.
Wholistic Visibility
Microservices can result in an overly fragmented approach to implementing your solution. With so many services doing different things, it can be difficult to understand the business flow behind your implementation.
Tracking the flow from system to system can also get complicated and often times requires centralized logging frameworks (like Spunk) and correlation ids to track the activity for your system.
Is there any disadvantage of microservices?
YES. Apart from listing the "cons" above, let me share a personal story...
I was part of a financial team responsible for sending and receiving payments. We spent nearly a year designing the system around a microservices.
Each service fit into one of three categories: listening for messages, filtering messages, and distributing messages. While subtle differences surrounding message type and downstream system existed, we were able to leverage the same implementations (listener, filter, distributor) for every service.
This approach was great at first. A team of 5 developers was quickly able to implement a collection of 15+ microservices sending and receiving payments. We were witnessing first hand the advantages of microservices. Any developer could easily jump into the code and make changes. When issues were found with a service, it could be independently fixed and deployed. When volume for a certain message type spiked, we could scale just the services that needed the extra resources.
It was perfect...
Fun turns to tragedy...
As we kept cloning more listeners, filters, distributors we started finding issues in our code base. Each issue we found required fixing the same code 15x times as the implementations were largely the same.
We also had to make frequent security updates to stay compliant. This usually involved updating dependencies our services relied on. While it wasn't too complicated to update versions to more secure versions, doing this 15x times became time consuming.
We also started facing issues with message brokers being down and network calls failing when calling downstream systems. This introduced a lot of complexity with respect to resiliency. What were we supposed to do if a listener consumed a message from Kafka but then failed to send it to the next microservice?
Such issues required updates to our design which then required 15x updates to our system. As our design evolved over time, the work to update our existing implementation became exponentially time consuming.
Tracking payment messages also became a nightmare. We established a correlation id to track messages from listeners to distributors but, once again, this introduced complexity in terms of tracking information and developing tools to visualize and track these ids.
In the end, we agreed that managing the system as a monolith would have been much easier. We could have more easily made updates, addressed security concerns, and understood what was happening from a single implementation.
In hindsight, many of our issues could have been alleviated by using common libraries. We could make updates to code and vulnerable dependencies once. However, this approach would have also introduced its own challenges. Updating each service with the updated lib version could result in the same "duplication of work" and managing all of the network hops and resiliency would still remain a challenge.
Who are microservices best suited for?
Microservices are best suited for teams that need them based on the challenges they are facing. In other words, don't implement a microservice based approach just because it's the popular thing to do.
It's often better to start with a monolith, especially if you are a smaller organization or just getting started. Just because your system lives as a single process doesn't mean you can't establish architectural boundaries with interfaces.
Remember that you can always physically separate your services as your system grows. If you follow good coding practices and standards then migrating to microservices later on won't be extremely difficult.
Conclusion
Microservices are great but don't jump into them too quickly. You'd be surprised how far you can get with a well coded monolith. You can always adopt a microservice architecture later on when it becomes more obvious what functionality should exist as it's own service.
Don't implement microservices "just because". Adopt them over time and as needed to avoid the pitfalls of using a distributed design including resiliency, coordination, and visibility.