Microservices are becoming a more and more important topic in Software Engineering throughout different industries. They are easy to implement and maintain if done in the right way. A Microservice Architecture is always a distributed system and brings a need to focus on completely different parameters than those of monolithic applications. Deployment monoliths are the opposites of microservices and are increasingly being replaced by them. Replacement is only possible because of the nature of microservices to be distributed. In this document, we will first talk about this topic in general because there are principles which are valid for all technologies. In the second part, we will focus on a Spring Boot implementation, where the principles will be shown and proven.
Microservices are small applications maintained and evolved from a small cross-functional team. It is important to note that a microservice is per definition self-contained and without dependencies, a fact that should be considered already in the design phase. Some people may know this from DevOps, so microservices are a great option for DevOps. Usually, microservices are operating in a distributed system. This is the complete opposite from big monolithic software, which usually has one big deployment artifact. Microservices can be fragments of this which act to supplement it, but can also function as a separate operational structure. My practical experience has led me to the following insights to ensure that microservices end up in a healthy and stable distributed environment.
1. A microservice should be stateless
A microservice should be scalable. The easiest way to preserve this scalability is to design it stateless from the beginning. This means, when the traffic has peaks, we can scale the service with ease without hesitating to consider whether the state of one instance of our service is in conflict with other instances. This is also a limit to a possible persistence layer of a microservice because transactional solutions can be the death of a system optimised for performance and vice versa.
2. A microservice should not have dependencies on other services
A microservice should start up and run independently, which means stability if other services are not available (a standard scenario in a distributed environment); the service should do its job and should not fail to start up or stop the service completely. This means that the architect as well as the developer should deal with this scenario from the beginning.
3. A microservice must be change resistant
A microservice has an interface and uses interfaces to communicate. These interfaces should be as stable as possible. Regardless of what happens to the services inside and what is changing there, they should understand each other at all times (the choice of the interface technology is also very important here). One way to achieve this is by consumer-driven contract testing.
4. Build and deployment processes should be automated for each microservice
The build as well as the deployment should be completely automated. The complexity of bringing the latest changes in production must be as low as possible. This means that even a non-developer could execute the process to deploy a new version on the productive system. This also means that unit tests, integration tests and perhaps also system tests should be integrated in the build to avoid any nasty surprises at the finish line.
5. High and common developing and quality standards in microservices
It is the nature of a distributed system that there is quite a high possibility that different technologies are in use. But no matter what technologies are in use, all teams must have a common understanding of:
What is a microservice?
How can we obtain high quality?
With which teams (microservices) will our system collaborate?
How will our system communicate (asynchronous, synchronous)?
What are the technical limitations (e.g., message bus is mandatory to use)?
What are the interfaces (consumer-driven contract testing)?
This invariably leads to a situation where team members cannot change as quickly into a new team as they could in the case of monolithic software. But you are also not bound to one technology and can change it whenever you would like to do so.
6. Great people
Never underestimate the value of good people in a project. Great developers with strong communication skills are needed, especially in a distributed environment, where miscommunication can lead to disaster. Also keep in mind that bad or average employees will never produce great software! That means you must not only choose the right people from the beginning but also train and motivate them. Having great people is a precondition you should have regardless of the architecture or technology.
2. Pros and Cons of microservices
The pros and cons of microservices are widely known, since they are the same as those of a distributed system. Regardless, they will be shown here in a short table. For me as consultant in IT, these are not pros and cons - they are more challenges and benefits, because I think if we manage the challenges, we can really gain from that.
|More independence of different teams in different projects, hence more agility||High collaboration still needed to achieve the goals of the “end product”|
|Continuous delivery easier for each service but also for other services due to separation of concerns||CI is also possible with a big deployment artifact, but much harder to achieve|
|Separation of concerns is much easier to achieve||“Shared Data” problems are easy to solve in monolithic applications but hard in shared services|
|Scalability of each service on demand||A service is only as scalable as the weakest service in his chain (bottleneck)|
|Independence in case of failure in one service||You must deal with the cap theory|
|High testability due to the fact that we can test one service and have contract-driven testing for corresponding services||Building the system needs much more care and it must be clear what the purpose of the system is|
|New technologies can easily be tested and won’t have a risky impact on the rest of the system|
|Time to market decreases|
|Because you can scale, you can avoid error-prone parallel processing in your application|
3. Set up a Service with quality gates
In this section, we will leave the theory behind and get to a real microservice. But we will not only show how to build up a microservice from scratch; we will also show how we can introduce quality gates from the beginning. Especially in shared environments, great care should be devoted to quality from the beginning. The plan is to implement a Spring Boot microservice from scratch and show this points before based on this practical example. Please note this is only an example of best practice; this is not the one and only way to do it, so take it more as a recipe than a construction manual. All examples can be checked out and explored from my GitHub account. ( https://github.com/mburkertt) All applications start with getting a raw application. This application should be runnable. With Spring Boot, this is quite easy. After starting the spring initialiser https://start.spring.io/, you will see a nice graphical user interface. This interface not only gives you the possibility to set up a raw application in java but you can also build a Kotlin or Groovy project and there are other different options you can choose. What we will do is build up a basic Java application with a maven build and no dependencies at the beginning (see figure 1).
Figure 1: The Web Spring Initializr
The next step is now to get a common code style in the project and make sure that all people will stick to the standard. We will do this with checkstyle https://maven.apache.org/plugins/maven-checkstyle-plugin/. Checkstyle is a plugin for maven and gives us the possibility to easily check the code for many kinds of violations. In the base setup of checkstyle, the Sun and Google standards are already available. In the example, we will use the Google standard (Sun is extremely strict). We must add a checkstyle folder and a checkstyle file there (see figure 2). Additionally, we can add a suppression file to order the files or packages we would like to exclude.
Figure 2: Project structure and position of checkstyle files
To integrate this in our build, we need to add it to our pom file. The pom file is the description file of our build. The support for checkstyle in maven is quite good and so we have a build plugin for checkstyle (see figure 3).
Figure 3: Maven checkstyle plugin build integration
After this, we execute the build to check if everything is working, and if it is working, we can go to the next step. To have good code quality, it makes sense to have a static code analyser. In our example, we will use findbugs for that http://findbugs.sourceforge.net/. Findbug is like checkstyle – quite easy to integrate in the build process (see figure 4).
To prove the work for the first project, we will implement a very simple use case with test data, and more importantly, we will implement tests for that (see figure 5).
Figure 5: Configured test data
We use a simple string for this. There are a lot of things we could do with this test data. First, we will build an object representation of our data (see figure 6).
Figure 6: Object representation
After this, we should implement a service which can manipulate this data in a measurable way (see figure 7).
Figure 7: Service for data manipulation
For our business logic, we need unit tests to verify them. Due to the fact that we have a very simple logic in this example, we also have quite simple tests (figure 8).
Figure 8: Test class for our business logic
Now, as we have the first example running, we should think about integration testing. We need to do integration testing when a simple unit test is no longer sufficient. This could be the case when we have external systems or modules in use. One example of this could be a Database. If we have a Database and must program statements for it, we must verify that these statements are delivering what they should. We also must verify that the Object relational transformation is working correctly, and this can be a pain. To have this quickly reliable and to secure ourselves against regression, we will use integration tests. To achieve this in our microservice, we need to add a database first. For this, we have a spring config file (see figure 9).
Figure 9: Configuration for database
We will also need a database for this. We can easily obtain this; we only need to create a schema.sql file in our resources and put the sql for the needed tables and rows inside.
But be careful: you should perform a create table if it does not exist, otherwise it will try at every start to create a table that is already there. In our example, we will only have the simple table person. This table will have standard functionality from a JPARespository. To have this available, we need to add
the dependency in our maven pom.xml. Our repository now has some standard functionality. We won’t implement our repo; the standard functionality is enough to show what we would like to show. We can integration test the available database logic now as well as the mapping. In our case, we do that with spring integration tests (see figure 10).
Figure 10: Spring integration test example
But what should be done when there is no suitable integration system? Usually, when there is no suitable integration system, we use a Mock. For Databases, we can use DB unit to achieve that. DB unit is a Java library to emulate databases during unit test runtime. We must add them also in the pom.xml as a maven plugin as well as database xml files in the test resources. The test becomes a simple unit test file then (see figure 11). As you see in figure 11, we start the spring context, but then we do not load the database like in an integration test, but rather load the database setup from xml. It can be more suitable to load an xml for a specified scenario instead of a whole test class, but if you would like to do that, you only must put the annotation on method level instead of class level. When the database is there, we can simply execute our repository methods like in runtime. That also means that it is easier to execute the tests in a build environment.
Figure 11: Test example with dB unit
Microservices represent a possibility to have quick results and a standalone running software. However, distributed systems are very complex and need quality gates more than monolithic systems. Also, a distributed system should be well designed. But on the application level, microservices make the developers’ lives much easier. As long as you meet general quality gates, and do not touch the interfaces, you will develop much easier and accelerate much more than in monolithic projects. Another important factor is that it is easier to innovate with new technologies and the impact of a failure is much smaller. So from an objective point of view, it is worth it to slowly change your architecture to microservices.