In my last blog posts (Microservices: a few thoughts and The Importance of structuring Microservices) I wrote about microservices. Now it's time to go from theory to practice and write some code. Based on Spring Boot, Netflix OSS (Eureka, Feign, Hystrix and Ribbon) and Maven I created a setup for a microservice architecture.
I have put my example microservice project on GitHub.
What the microservice example does
What the microservice example does
- It's a microservice system consists out of 3 microservices and one frontend microservice.
- product service: which just returns the data for a product (CPU, keyboard or mouse).
- user service: which just returns the data for a user.
- shopping cart service: which stores for the users the products in their shopping carts.
- frontend service: communicates with all microservices, aggregates the data and generates out of the data a simple HTML page.
- The microservices are grouped by domains.
- Eureka is used to discover the microservices. Multiple instances can be started and new instances will automatically registered into the system and will be used from the existing microservices.
- Ribbon is used transparently to do the load balancing on the client. So there is no need to have static IPs and a manually maintained load balancer.
- The REST clients for the microservices only consists out of interfaces. No cooding needed.
- Hystrix REST clients are used to ensure resilience. The Hystrix REST clients are very effortless to write.
- The REST endpoints are done with Spring`s @RestController and implements the interface of the REST Client to ensure that the REST client and the REST endpoint matches each other.
- Enhanced log output to make you life easier. It's done transparently with a Spring Interceptor and a Feign Interceptor.
- The frontend microservice generates a UUID for each request. This UUID is put into the MDC (Mapped Diagnostic Context) and will be printed out for each log entry. The UUID is passed as a header to the every called microservice - even if the called microservice calls another microservice the UUID is passed along too. Therefor it's possible to know which user request caused which log entry in any microservice.
- Additional a calling stack is generated and logged. The calling stack looks like this: ServiceB <- ServiceA <- /product/1 <- 127.0.0.1
- /product/1 <- 127.0.0.1 is generated by the frontend microservice and shows the IP address of the request and the called endpoint
- ServiceA: the frontend microservice called the ServiceA
- ServiceB: the ServiceA called the ServiceB
- => Therefor you always know who called the microservice. Which isn't always a easy to answer question in a big microservice system. You could improve it furthermore if you would include zipkin.
- It's possible to pack all the microservices together into one monolithic application and this application works without the need to change anything. Therefor you have great flexibility. For example in development you probably don't want to start the complete microservice system on your machine. It's handy if you can just start up everything you need, which could be only a part of the system, packed into one application
Overview: Main Frameworks
I give you an overview over the main frameworks/libraries and what they do.
Spring Boot: It saves a lot of time for the setup. The auto configuration and the spring-cloud-starter-* packages are awesome. In addition the Dependency Injection and the Spring Feign abstraction are great, too.
Eureka: It's a service registry. Each service instance registers itself to Eureka. Afterwards all instances of the services can be requested just with the name of the service. So there is no need for IPs or ports. If another service instance is started then it will be added into the corresponding service group or if a service instance is shutdown then the instance will be removed from Eureka.
Ribbon: It's a client side load balancer working together with Eureka. Spring Boot transparently integrates Ribbon. So you have to do nothing to use it. Ribbon uses the round robin scheduling algorithms.
Hystrix: It's a latency and fault tolerance library designed to isolate points of access to remote systems. I am using annotations to configure Hystrix. It's very easy to use Hystrix and does only require a very little amount of effort. By all means the programmatically side is very easy. The hard part is to react correctly to errors.
Feign: It's a REST client based on the Apache HttpClient which has a great level of abstraction. With Spring the only thing you need to do is to write an interface for you REST endpoint and annotate it with @FeignClient. You can use the Spring MVC annotations instead of the Feign annotations. This has the advantage that you reduce the complexity and the @RestController can implement the interface to ensure that everything is compatible.
The top level modules of the project
- all-in-one: this module includes all microservices and it is fully functionally without any change. Eureka isn't needed anymore and all remote calls will be done with standard Java method calls.
- common: this module contains some common code and following microservices: Eureka, Hystrix Dashboard and Turbine Dashboard.
- frontend-service: this module generates the HTML and calls the other services to get the data.
- product-domain: this module represents the product domain and contains all product microservices.
- user-domain: this module represents the user domain and contains all user microservices.
One repository to rule them all
I deliberately put all microservices into a big project. The reason is that it makes many things easier:
- The need to clone only one repository. Especially for this example no one would like to clone several repositories.
- Refactoring, searching, code navigation and so on works very well if you have everything in one project. Especially if you run the all-in-one monolithic application then it's very handy for debugging and coding.
- Changes can be done globally or locally. If you want to upgrade one library because of a security issue then it's very nice if only one pom.xml file needs to be changed. For example you could change the Spring version for all microservices. Otherwise you would need to clone and to open several projects and fix them all. Still Maven allows you to change the version of a dependency only for a specific module. You have the flexibility. Each module/microservice can be configured individual or mostly everything can be configured globally.
- Only one build is needed. Depending on the size you could build always everything. Or individual domain groups. Or individual microservices. Just execute mvn install wherever you need it.
I would extract the common module into an own repository. This module shouldn't change that often or is interesting to debug. Therefore it shouldn't be painful if it's separated.
With the current structure there is one problem: you can't build the project directly after you have cloned it. It's necessary to build the common/common-configuration module first. Because it creates an artifact which is needed and sadly Maven can't resolve this dependency correctly.
If the project begins to grow then possible it makes sense to split the project further more. For example you could create an own repository for each domain-module. But it's up to you.
Each microservice is located in a domain-module with the exception of the frontend-service. Because the frontend-service aggregates all domains. In order to have a consistent design and usability. In the frontend-service should only be very little logic. It should only requests and aggregates data from the endpoints and prepares the data to be displayed. But it shouldn't have any business logic.
A microservice consists of two parts:
- microservice-client: contains the Feign interface for the REST endpoint and the model/POJO classes. Additional there is a Hystrix-REST-client which just wraps the Feign-client. Actual everyone should use the Hystrix implementation instead of the Feign client.
- microservice-service: is the microservice itself with the business logic and the REST endpoints. It has a dependency to the microservice-client. Since the model/POJO classes are needed and it implements the REST interface. Thereby fewer mistakes can happen.
I am using my parent-pom for this project. This POM file enables several useful features like static code analysis.
In common-configuration the logback.xml is defined. The file will be packed into an Maven artifact and then extracted into each service-module. Thereby it prevents to have several copies of that file. Probably you could also move the application.yml and the bootstrap.yml into this module. Otherwise there is nothing special to the Maven setup.
Scripts and Execution
To make it easier to build and run the project I provided some scripts. They are located in the scripts folder and are very easy. So I think I don't need to explain them.
The only thing what is important to know is that it can take a little while until the microservices are registered correctly and all microservices received the instances of the other microservices. In the case of an error just try to reload the page a little bit later.
I think it's a quite sophisticated setup and that it is very well suited to be the basis of production microservices. One amazing thing is that it's possible to pack all the microservices together - as long as the dependencies are compatible. This makes supposedly the development easier. Instead of the need to start many microservices you just need to start one application. In addition it can be a quick fix for performance issues. If there are two very talkative microservices just glue them together until you fixed the performance issue.
I hope you like my setup and my blog entry. I probably will write another blog post to explain some details of the setup. This article is already so long that I didn't want to include more details.
I am always happy about comments and suggestions for improvements! :-)