How to design microservices architecture?
Microservices are a holy grail for some and a pitfall for others. A lot of companies are leveraging them as core of their systems, some try to migrate to, some try to migrate away. When you ask someone about their attitude toward this architectural style you'll probably end up with polarised opinions, depending on who you ask.
I'm a Software Architect at SoftwareMill and I've been designing and consulting microservices implementations for the past few years. They are not a perfect solution to every problem, sometimes a well structured monolith is better. How to investigate if your system would benefit from microservices without jumping right away into a binary approach? Here are my lessons learned and thoughts on microservices.
What are microservices?
Microservices are one of the architecture types you may leverage in your system. They are related to creating multiple, smaller, loosely coupled services, which can be developed and deployed independently.
I've encountered an interesting definition of microservices on Uber's blog. It refers to SOA (Service Oriented Architecture), saying that microservices are an extension of it, however they include smaller, better scoped services. What I like about this approach is that it mentions a few important qualities: independent deployments and scaling, which are especially difficult in Monolithic applications.
When talking about microservices we focus on small, independent services automatically managed and deployed. We can find similar perspective in Martin Fowler's definition:
The term "Microservice Architecture" has sprung up over the last few years to describe a particular way of designing software applications as suites of independently deployable services. While there is no precise definition of this architectural style, there are certain common characteristics around organization around business capability, automated deployment, intelligence in the endpoints, and decentralized control of languages and data.
What are the benefits of microservices?
Before you even think of migrating to microservices you need to calculate if their benefits outweigh the challenges for the application you are considering. Only then, we can start talking about the benefits of using microservices. They can introduce flexible solutions for the following challenges:
- Loose coupling allows for independent deployments & scaling of different system parts.
- Changes or local refactorings are easier, just because codebase per service is smaller and less complicated.
- Performance issues are easier to locate (assuming that you have proper monitoring & tracing).
- Team working on a single service goes smoother, so the number of git conflicts is lower and the code knowledge is much higher.
- Work with smaller codebase, may allow you to bring features to production quicker.
To get to know more info about microservices benefits, take a look at What are Microservices and what are their benefits?
Microservices lessons learned
Considered starting from monolith
(…) you shouldn't start a new project with microservices, even if you're sure your application will be big enough to make it worthwhile.
New projects (especially startups) have a lot of unknowns. Companies' businesses can transform in not predictable ways. Starting with microservices adds additional overhead during refactor or rewrite of created system. It may be more beneficial to start with monolith PoC (Proof of Concept) or MVP (Minimum Viable Product) to see how it works and how it is perceived by users in order to later adjust the features and migrate to microservices.
What is more, not every company ends with microservices. Some migrate to them and … later back, because it appears they haven't solved their problems.
Let's say that you have 50 microservices. Can you imagine a situation in which you manually specify on which machine runs which service? Or one in which you set up different environments versions for different services on production services, e.g. on server A let's install NodeJS 8, on server B NodeJS 12? It wouldn't be pleasant, for sure.
Nowadays an industry standard is to use containers together with a proper orchestration system of automatic deployments and resource management. The most common choice is Kubernetes. It can be self deployed, but not necessarily. Biggest clouds offer hosted k8s environments - AWS EKS (Elastic Kubernetes Service), GCP GKE (Google Kubernetes Engine) or AKS (Azure Kubernetes Service). This has an additional advantage - if your business decides to move to a different cloud (yes, I have seen such a situation), then the migration will be easier.
CI&CD (Continous Integration & Continous Delivery)
When you have a lot of services, the thing you want to achieve is the lowest ops overhead related to adding a new one. In optimal situations everything should be automated. Continuous integration (CI) which automatically builds code when it lands in the repository. Deploys happening automatically when e.g. commit is marked with a specific tag. For sure no manual jar's copying to production servers!
You need to remember about other topics like log aggregation, monitoring, alerting or tracing. Those are things definitely usable in microservices architecture which will save you when something will go wrong. However, someone needs to maintain them, so it's good to have a DevOps team responsible for that.
Keep your service stateless
If it is possible, avoid keeping local state with your microservices instances. For sure to avoid down times your goal is to have always more than one instance of the service running. In order to save money, both of them should be active (not active-passive). To do that, they can't just keep anything they want in the memory.
Remember that it does not only affect what you keep in memory, but also how the service processes the data. You may think about situations when there is a single "Business Processor" in the system which operates on incoming data. Try to redesign it from the start, so that it won't become a bottleneck later.
There are some patterns allowing to avoid such situations:
- keep every state in database,
- use distributed caches,
- choose e.g. Akka Cluster with Akka persistence,
- and others.
From the start you have to think about scaling. You won't avoid it, but you can minimize the impact if you design your service while keeping that in mind. There is a big probability that at some point there will be more than 1 instance of the service running.
This point is quite controversial. When you have multiple services using a single type of database, let's say Apache Cassandra, it's quite common, to have a single database cluster for the whole system, for all microservices. For sure it allows to lower the operational costs, but also brings drawbacks:
- when one service is using the database more heavily it is often quite difficult to debug which one is doing that. What is more, the traffic it causes can influence the performance of other services.
- when you need to perform some maintenance which will require to shut down the database, then all your microservices are down at the same time.
So what to do? You have to decide by yourself and find the best tradeoff between costs and related possible issues. As a minimum keep tables isolated in separate schemas/keyspaces. This will allow to avoid name conflicts and make migrations to separate clusters in the future easier.
Don't force practices leading to distributed monolithic architecture!
In bigger companies there is a tendency to do everything "the same way". Same style of code, sometimes the same technology stack. Unfortunately, I have seen cases when it went too far - the same dependency versions in all services or a shared common module. Things like that seem innocent at start, but later it appears that they bring coupling, which is difficult to get rid of later.
Let's say you'd like to upgrade Akka in service A? sorry, first you need to upgrade common, but then you need to upgrade service B, C, D…. agrh. Another difficult scenario is a "functional" coupling, where adding a new business feature requires changes in a big number of services. This may indicate that service boundaries are set wrongly. In order to get loose coupling you may also consider asynchronous communication based on events.
Distributed monolith is one of the worst things you can achieve - you get all drawbacks of monolith and microservices combined in one. Making simultaneous changes in multiple services is much more difficult than introducing the change in a single monolith. Microservices architecture assumes that services are independent, remember about that.
Give some space for experimentation
Smaller services make the experiments cheaper. Maybe developers would like to try a new framework? Maybe you consider using a different language or database?
Why not try it in a new simple service? If it appears to be a mistake, rewriting it with a different stack will be much cheaper.
I won't say that microservices are perfect, because they are not. But in many cases Monoliths are even worse. You need to learn from your mistakes, when it is the best to choose the right approach. Keep in mind that when you go the microservices-way, allow people to use all the possibilities created by that choice. Services should be independent and decisions made do not have to be exactly the same in each of them.
If you'd like to learn more about microservices then you can also take a look at our other related posts:
- Are you sure you're using microservices?
- What are Microservices and what are their benefits?
- What are the essential skills for Microservices developers?
- How to communicate your Microservices?
- 6 reasons why not to use Akka Cluster for interservice communication in a microservice architecture
- Is your infrastructure ready for microservices?