Practice shows that with the continued relevance of the microservice paradigm, there is no shortage of its interpretations, criticisms, and even debunking. Therefore, returning to our translated publications, we decided to talk specifically about microservices, or rather, consider a detailed answer to the question posed in the title of the article.
The term “microservices” first appeared around 2011/2012. Since then, without microservices in the IT environment, almost nowhere. However, after so many years, have we managed to properly understand what microservices are?
First, let's look at a few definitions:
Microservices is an approach to software development (…), according to which an application is structured as a set of loosely coupled services. In the microservice architecture, services are highly detailed, and the services used lightweight protocols. The benefit of decomposing an application into individual smaller services is to improve modularity. In this form, the application is easier to comprehend, develop, test, and the application itself becomes more resistant to erosion of the architecture. With this approach to architecture, development is parallelized, and small autonomous teams get the opportunity whatever develop, deploy and to scale those services for which they are responsible. Also with this approach, continuous refactoring can be applied to the architecture of each service. Microservice architectures are also designed for continuous delivery and continuous deployment.
Here's another passage from Martin Fowler:
The term “microservice architecture” has been firmly rooted in the last few years to describe a particular way of designing such applications, each of which is a set of independently deployable services. Although there is no exact definition of such an architectural style, there are a number of general characteristics regarding the organization of the application around its business capabilities, automated deployment, collection of information on terminals and decentralized language and data management.
You probably noticed that the main similarity between these two formulations is the independence of services and options. Now let's try to answer a few questions and try to find out if the system you are actually implementing has a microservice basis!
Does your system allow you to restart services independently?
Or do you need to restart services in a certain order? This behavior can be dictated by specific cluster systems, where the node connects to the seed node, for example, in the Akka cluster, if it is not handled properly. Other cases may be associated with long-playing connections or sockets that can only be opened on one side. Remember that communication should remain simple and lightweight. Even better if it is completely asynchronous and message-based. All the restrictions that cause the services to depend on each other in the long term can turn into difficulties with support. Problems with one of the services can cause problems in all other services that depend on it, which may someday result in a serious global failure.
Can you deploy services independently?
There are cases when the release and deployment of all services in the system should occur simultaneously. This process takes several days. Of course, in this case, the possibilities of introducing continuous deployment are limited, delays occur, and the responsiveness deteriorates. It takes too much time to deal with urgent bugs, simply because of the costs associated with the deployment process. When different teams are busy supporting different services, any deployment dependency can prevent other teams from deploying their services, which will cause the work to be suspended, and its effectiveness will decrease.
Can a modification necessitate synchronized modifications to other services?
One can imagine many reasons leading to this behavior. First of all, these are API changes. It happens that it’s enough to add a new field to the API request to ensure compatibility between services. However, there are workarounds to help deal with such situations:
- Perhaps you want to maintain mandatory backward compatibility for all changes made to the API; that is, whenever a fundamental change is made, a new version of the API is added. This does not mean that you will need to always support many versions of the API; you will only need them for the period until the migration is completed. You can simply talk to other teams and make sure everyone has completed the migration, or turn on metrics to judge whether a specific API is still being used.
- You can also consider this option: deploy several versions of your application at once, allowing other services to switch to the latest release, and then disable the old one.
- Also try using formats that support the evolution of circuits, for example, Avro, where when adding a new field compatibility may not be violated. When a value for a field is missing, the default can be used.
Another reason for synchronized deployment is the use of shared code by different services. Depending on how the release process is built in the shared part of the code, any change may require updating in all services.
Does a database change cause a change in many services?
Yes, communication through a database is a well-known antipattern. In such a situation, it is necessary to update the scheme and simultaneously change the code of several microservices at once. It is better to ensure that only one service is responsible for managing the circuit. In this case, it becomes much easier to maintain the database (that is, perform its updates) and implement releases. If you are already in a situation with shared database usage, then try to limit the number of write operations so that another service works only for consumption.
Can you update a dependency or version of Java in a single service?
In theory, it should always be possible, right? But really not. There are many examples of “shared” libraries or just projects that contain definitions of all the dependencies used in the system. The goal of the microservices paradigm is to ensure that each of them is an independent entity, moreover, different microservices do not even have to be written in the same technological stack. When starting a new project, you should be able to decide for yourself which technology is best for it. Any binding has new problems. At some point, you may need to upgrade to Scala or JDK, but of course you do not want to do this right away in the whole system. In a system where the level of detail is correctly observed, you can update an individual service, deploy it, test and monitor it for several days or weeks, and then, if everything is correct, you can perform updates in the rest of the system. However, when there is
common or other strong unification mechanisms, such possibilities are very limited.
Does your chosen framework or library force you to dwell on the same choice in other services?
Some libraries are quite invasive. When you decide to integrate them with external systems, it may suddenly turn out that your choice is not so great when you are going to implement a new service in a different technological stack. The costs associated with its integration in the same spirit as the rest of the services may simply be unbearable.
Suppose, for example, you have several services written in NodeJS that actively use JSON: API. Next, you want to implement a new service on Scala and it seems that there is no suitable client library (a few years ago we had a similar case, but now the situation in this area has improved a bit).
Does failure of a single service cause a failure of the entire system?
Suppose communication in your system is completely synchronous. One service makes a request to another and waits for a response from it. If the first service fails, then the second service will not be able to work at all. If this is exactly the situation in your system, try to implement asynchronous message-based communication in it. It may be a little harder to simulate all the processes, but this approach makes it possible to smooth out the effects of failures.
In cases where this is not possible, you can try to temporarily limit the set of features of the application. For example, on the page of an online store displays information about the product and reviews on it, moreover, product information and reviews come from two different sources. If the reviews microservice does not work, you can still display the product page by simply informing the user that the review system is temporarily unavailable.
Is there any shared component on your system?
Do you have shared code? Configuration? Anything? Yes, this is not a trivial question. Advocates of the cleanliness of microservices believe that it is best not to allow shared entities in the system at all! It’s better to just copy the code than to split it. In practice, it is sometimes considered acceptable to have several shared libraries, but with strict limitations. Perhaps these will be libraries containing files
proto for protocol buffers or just plain POJO objects. Better avoid dependencies in such libraries. We had a case when a simple client library that caused dependency conflicts in a large system. At some point, it began to seem that the oldest elements of the application were using the old HTTP library, which could not be updated. Fortunately, these situations can be avoided by using renaming dependencies in the build process (dependency shading), but be very careful.
The harm from over-cohesive services is far more harmful from problems caused by code repeatability.
Sam Newman, Creating Microservices
Implementing the perfect microservice-based system is no easy task. People tend to “cut corners” by introducing tight ties into the system arising from shared code, and at some point such a system in practice turns into a distributed monolith. Often this happens if microservices begin to be used already at the start of the project, when the subject area is still not well studied, and you do not have a solid ready-made background in DevOps. It is better to start with a properly structured monolith, where all areas of responsibility are fully known, and they can be easily separated from each other. However, from the very beginning you need to be sure that your system is designed cleanly and that the package organization is kept in order, which subsequently will provide you with easy code migration to new services (for example, a single “almost top-level” package can be the basis for a new microservice) . ArchUnit can help with the analysis of dependencies that arise between packages.
(…) you should not immediately begin work on a new project with the introduction of microservices, even if you are sure that your application will be large enough so that the microservices justify themselves.
Remember, microservices should simplify both development and support! All you need is loosely coupled services, each of which can be deployed independently of the others.