Previously, we’ve looked at a thing called monolithic hell that developers of large applications built using the monolithic architecture style often end up in, and we’ve seen the problems of that architecture style. In this second blog post, then, we’ll take a look at how the microservice architecture addresses those problems.
The following content was summarized from one of the chapters in Chris Richardson’s amazing book on microservices, Microservices Patterns. If you’re interested in advanced microservice topics such as how to address the drawbacks of the microservice architecture mentioned below, I absolutely recommend taking a look at it. It’s available on Amazon or from Manning.
- What This Blog Post Is Not
- Term Definition
- The Architecture’s Benefits…
- … And Drawbacks
- By The Way…
What This Blog Post Is Not
Before we get started, here’s a short disclaimer: This blog post will not be a collection of unreflected appraisals of the microservice architecture that tells you it’s the solution to all your problems (it quite clearly can’t be since we all know there are no silver bullets and there is no free lunch). In particular, it’s always worth keeping in mind that the functional requirements of a piece of software can be achieved with almost any architecture – it’s usually only the nonfunctional requirements, or quality attributes, the architecture choice influences. So, if your application fails to meet functional requirements, the problem is most likely not the architecture it was built with, and switching to a microservice architecture just because it’s currently en vogue will probably not solve the problem.
With this out of the way, let’s dive in.
First, we must define the term microservice architecture. On a very high abstraction level, a microservice architecture is an architecture style that decomposes a real-world business domain into smaller subdomains. Consequently, an application built using that architecture style providing some functionality within that business domain is sliced into smaller functional units or services.
A microservice, then, is all code dedicated to achieving a certain function within one subdomain of the overarching business domain – we could say each microservice is a boundary for a single set of cohesive responsibilities within the larger domain. So, a microservice does not necessarily have to be micro in terms of its code base, it’s only micro in terms of its responsibilities.
The following are some key characteristics of microservices and the microservice architecture:
- There is one code base for each service and each one yields its own executable, hence services can be developed, tested, and deployed individually.
- Each service encapsulates its set of responsibilities behind an API that other services can consume.
- Each service is responsible for its own small share of the overarching domain and therefore has its own data model as well as its own data store.
- The services in a microservice architecture are typically built using light-weight, open-source components.
- With microservices being dedicated to only one function within the business domain, applications built using the microservice architecture can consist of dozens or even hundreds of microservices.
The Architecture’s Benefits…
The microservice architecture has come to stay, and there are a couple of very good reasons for that:
Enabler For Continuous Delivery And Continuous Deployment
The microservice architecture is an enabler for two important aspects of DevOps, Continuous Delivery and Continuous Deployment. This is because Continuous Delivery and -Deployment require a high degree of test automation and deployability, and the microservice architecture improves both.
In a microservice architecture, each service is comparatively small, so it’s a lot easier (and more fun) to write good tests for it. With each service being well-tested as a result, the overall application has higher testability, too. Deployability increases as well since changes to one functional aspect of the overall application are restricted to one service, and so the developers responsible for it can deploy their changes without having to coordinate with other teams. Also, because deployments become so easy, changes can be deployed at a much higher rate, even to production. This, in turn, results in lower time-to-market (management folks tend to like this very, very much) and thus faster gathering of feedback of the deployed results from stakeholders and clients.
Lower Communication And Organization Overhead
The microservice architecture also profoundly impacts organizational structure. The fact that each service is one cohesive “responsibility unit” implies a larger team of developers can be decomposed into multiple smaller teams, where each team is responsible for developing and deploying (that one is important – the traditional gap between development and operations is gone!) one service or multiple related services. And because the services interact with each other only via agreed-upon APIs, each team can operate autonomously on the code base of their service or services as long as the APIs don’t change. Consequently, communication and organization overhead drop significantly, and development velocity is increased.
Services Are Independently Scalable
Each service can be scaled independently of all other services, and it can be deployed and run on the machines that best suit the requirements of that particular service (think of one service that needs a lot of CPU and another one requiring a lot of RAM deployed to two machines accordingly). In a large monolith, different parts of the application might have such varying or conflicting requirements, too, but because they are part of the same code base and hence the same artifact, they must be deployed together.
Improved Fault Isolation
Have you ever witnessed a critical error in one microservice, such as a memory leak, crashing the entire application because the error has spread to its other microservices? No? Me neither. But have you witnessed something similar happening to a large monolith? Probably. So, a microservice architecture brings much better fault isolation – even if one instance of a particular service crashes, other services – and most likely even the remaining instances of the same service – will continue to serve requests normally.
Better Environment For Experiments And Updates
The microservice architecture provides a much more favorable environment for experiments. Whereas in a large monolith, you’re often restricted by decisions made in the past concerning programming language, frameworks, and other parts of the technology stack, in a microservice architecture, each service has its own technology stack. This means if you want to try out something new, you can simply build a new microservice. If your experiment succeeds, that’s cool, and if not, you’ve gained some important experiences and can just throw away the microservice without adversely affecting the entire application.
The same train of thought applies to updates of the technology stack’s components. In a monolithic application, an update can be very challenging because it would result in an incompatibility with some other component in the stack. Meanwhile, in a microservice architecture, each service has its own technology stack, so updating one of its components will only ever influence one service.
… And Drawbacks
There is no such thing as a silver bullet, and there is no free lunch – no architecture, method, or technology can solve all problems, and each advantage usually entails a disadvantage of some sort. The microservice architecture is no exception to those rules:
Decomposition Can Be Challenging
There is no easy-to-apply algorithm for how best to decompose a monolith into microservices. There are a couple of approaches available that can provide a rough direction, but even so, the decomposition is more a work of art than science or engineering. What’s worse is that if you don’t get the decomposition right, you’ll end up with a distributed monolith, and this is very, very bad – you’ll get none of the benefits provided by either the monolithic or the microservice architecture, but you’ll reap the disadvantages of both!
Increased Coordination Effort For Changes Spanning Multiple Services
The microservice architecture decreases communication and organization overhead as long as changes are restricted to one microservice (or multiple related services) because there’s typically only one team responsible for it (or them, respectively). In case a change affects multiple, unrelated services, though, multiple teams will have to be coordinated, which significantly decreases the overhead to bring that change to production.
Complexity Penalty Of Distributed Systems
A set of microservices is a distributed system, and as such suffers from the disadvantages this category of system traditionally has. First, there is the means of communication – services have to use some method of inter-process communication to talk to other services. This is not only inherently more complex than a simple method call, it also requires each service to handle partial or complete error of each remote service it relies on. It’s also a lot harder to implement queries that span multiple services, specifically transaction-type queries. Finally, writing tests that span multiple services can be quite challenging, too – not only on the technical, but also on the organizational level (if each team is responsible for one service and hence also implements the tests for that service, then who writes tests for the whole application?).
The microservice architecture is not all sunshine and unicorns – it brings with it significant challenges that need to be addressed. But, if correctly used, it improves development velocity and time-to-market through higher testability, better deployability, and less communication and organization overhead, and because of its divide and conquer strategy, it is an incredibly useful approach to tackle the complexity of large applications.
All of this improves the happiness of clients, developers, and managers alike – it makes clients happy because they get a reliable, highly scalable application built by an organization that can quickly incorporate feedback and respond to different demands or market situations. It makes developers happy because they can spend more time building the stuff that delivers value instead of having to fight fires because a bug made it to production again, or sitting in tedious meetings to coordinate with other teams. And if clients and developers are happy, then, well, management will be, too.
In the next blog post, we’ll acquaint ourselves with the fundamental building block of a microservices application, the application container, and we’ll find out how to build und use such a container.
By The Way…
We’ve talked about the value of decoupling things elsewhere, and you may have noticed the microservice architecture is, in essence, all about decoupling – it’s just that the decoupling is pretty radical:
- On the codebase layer, cohesive functional units are strictly decoupled from each other because they live in different code bases – there’s no way to accidentally violate the boundary of some abstraction layer.
- On the execution layer, once the microservices are up and running, they can only interact with each other via well-defined APIs, and even this form of coupling can be further relaxed by having the services communicate asynchronously via a messaging system (such as Kafka). The only kind of coupling left in terms of “talking” to each other is that the services have to “understand” each other’s messages.
- On the organizational layer, entire teams can be almost completely decoupled from each other since one team is only responsible for one set of related responsibilities in the overarching business domain and therefore has to know every little about the functional world surrounding its own microservice(-s).
So, another way of describing the microservice architecture would be it’s probably one of the most radical and profound patterns for decoupling that has emerged in software development history so far.