The concept of Microservices came out of a need of solutions to the problems with monolithic architectures. We refer to an architecture as monolithic if the entire app is built into one executable/package, deployed all or nothing, using one or very few data stores. These applications usually use tiered architectures (e.g. presentation, business logic, data layer) and internal modularization.
Monolithic architectures offer many advantages in developing small to medium web applications:
However, once the app reaches a certain age, size and complexity, the drawbacks of the monolithic approach become more significant:
What are Microservices?
Microservices are an architectural approach or style to designing large distributed systems aimed at solving the above mentioned deficiencies of the monolithic approach. Microservice architectures enable faster feature delivery and scaling for large applications.
The core idea of microservices is to split the large system into loosely coupled services that can be deployed independently. That’s it.
So what could the same monolith system from the example above look like in the microservices world? Here is an example:
In this diagram, each blue box is a separate service — a piece of code deployed independently and using the other pieces of the system through their external APIs. Each service in this architecture should follow these rules:
Microservices should be micro
That is, they should be small, specialized services with minimal set of features. But how small is small? There is no magic recipe, and it depends a lot on the domain and the team composition. Here are some guidelines:
Microservices should be as independent as possible
This should be very well covered under “deployed independently”, but many times too agile teams overlook this when rushing to pump out features. Its frequent that you end with a spaghetti-microservice architecture, with is architecturally a monolith with extra RPC calls.
Typical smells of this are:
Microservices should have their own data store
This follows directly from the requirement to be able to deploy independently. If many services are sharing the same data store, and a change requires modifying the data model, independence is lost. Services using the same data store are strongly coupled because of this. While there are valid use cases where 2–3 microservices need to share the same data store, if it's more than that probably some unnecessary coupling has crept in.
If your architecture diagram has a part that looks like this:
you are probably not doing microservices right.
However, the above pattern (many microservices using the same single data store) is a common approach to breaking up an existing monolith. If you treat this as a transitional phase of a larger plan, that pattern is actually very practical. It occurs as step 1 of the following general plan of breaking up a legacy monolith
1. Rewrite the service code as microservices, using the old database model. This allows to run the monolith and microservice code side by side on the same DB and test it for feature parity and regressions
2. Microservice by microservice, separate its part of the data in a new DB
3. Repeat 2 until monolith is broken down
Microservices are typically owned by a small team
One of the main advantages of Microservices is that new developers get on-boarded much faster. To get productive, they need to understand a much simpler system than their counterparts working on monolith systems.
To benefit from this efficiency it is best that one Microservice is owned by a small team. By “owned” here we are talking not only developing, but also deploying and monitoring of the service. Of course, one team can own more than one Microservice, and this is usually the case.
If a service requires a team of more than 10 to develop and maintain, it's worth considering if it is possible to split it in two services and two teams.
When Microservices are a bad idea?
Microservices impose tax on crossing service boundaries on both performance and engineering time. Every API call across two services is much less efficient than an in-process call in a different module: data need to me marshalled, the network stack engages, there is latency of the physical data transfer, and the network stack processing and unmarshalling happen on the other end. If it is a synchronous call, there is a thread waiting for a response, keeping stack and other resources allocated (if you went async, then the code is harder to write and you pay in engineering time here). The impact on latency is more severe, but throughput also suffers.
The engineering tax manifests in:
1. Added friction of writing distributed code. Besides the need to implement APIs/RPC, this means also losing static type checking at the API boundary. Since persistent state is distributed, you need to use distributed transactions (which are complicated) or manage failures without transactions (which is also not easy).
2. It becomes harder to implement features that span across more than one service. You typically need to implement downstream services first, and only when they are ready and deployed you can actually implement the UI. This requires team and deployment coordination, which negates the benefits of the microservices in the first place.
This tax pays for easier scaling of engineering time. It is very important to get the boundaries right. When you don’t understand the problem very well, which is at the start of pretty much every project, you are unlikely to get the boundaries correct. You are also typically starting with a small team that grows and has to build out the product. Therefore you should almost always start with a monolith, but look for well defined boundaries you can split into separate services as the team and the codebase grows.