Hello, habrovchane! Today we offer you another interesting post on the inexhaustible topic of microservices, this time for the luminaries and neophytes of the Java language. We read and vote!
In most microservice architectures there are plenty of opportunities for sharing the code - accordingly, the temptation to do this is also great. In this article I will share my own experience: I will tell you when it is appropriate to reuse the code, and when it is better to avoid it. All points will be illustrated with a sample
project using Spring Boot, which
is available on Github .
INTRODUCTIONBefore we talk about sharing the code and what is behind it, we will determine which tasks are usually solved using microservice architectures. Here are the main benefits of introducing microservices:
')
- Scaling is improved - different parts of the application are scaled independently.
- Effective elimination of strong connectivity between different parts of the system is always desirable, but is best achieved with microservices.
- The reliability of the system increases - if one service fails, the rest remain operational.
- Freedom in the choice of technologies - each service can be implemented using the technology that is best suited for this case.
- Improved reusability of components - services (even those that are already deployed) can be shared in different projects
- There are many other advantages, depending on the specific architecture or problem to be solved.
Naturally, many such advantages allow not only to build a better system, but also make life easier for the developer and make his work more grateful. Of course, you can argue about them as much as you like, so let's just agree on the fact that microservices are useful (which is confirmed by the experience of large companies like Netflix and Nginx). As with any other architecture, microservices are characterized by their own shortcomings and difficulties that need to be overcome. The most important are:
- Increased deployment complexity - the deployment process does not consist of one or several stages, but of dozens and even more
- More integration code - often services have to exchange information with each other. How to organize it properly would be worth writing a separate article.
- Will work in this subject area require actively copying code in a distributed system - or maybe not?
PROBLEMSo, here we come to the question faced by most of the teams that start working with microservices. Considering what the goal of working with microservices and recommended methods for their implementation, we face the problem: “We need weakly connected services, between which there will be almost no common code and dependencies. Thus, whenever we consume some service, we need to write classes that will handle the response. But what about the principle of "DRY" (Do not repeat)? What to do?". In this case, it is easy to hit two anti-patterns:
- Let's make the services depend on each other! Well, this means that weak binding can be forgotten (here we can’t definitely achieve it), and that freedom in choosing the technology will also be lost: the logic will be scattered throughout the code, and the subject area will become too complicated.
- Let's just copy-paste the code! This is not so bad, because, at a minimum, it allows you to maintain weak binding and does not allow the domain to overload with logic. The client can not depend on the service code. However, let's be honest; No one wants to copy the same classes everywhere and write a lot of stencil code whenever it is planned to consume this vile user service. The principle of "Sushi Code" turned into a mantra for a reason!
DECISIONIf you clearly articulate the purpose of the architecture and how the problem should be explained, the solution seems to suggest itself. If the service code should be completely autonomous, but we need to consume rather complex responses on clients, then clients should write their own libraries to consume this service.
This approach has the following advantages:
- The service is completely separated from the client, and specific services are independent of each other - the library is autonomous and customer-specific. It can even be sharpened for a specific technology, if we work with several technologies at once.
- The release of the new version of the client does not depend on the client; with backward compatibility, customers may even “not notice” the release, since it is the client that provides library support
- Now clients are DRY - no redundant code is copied
- Integration with the service is accelerating, but at the same time we do not lose any advantages of microservice architecture.
This solution cannot be called completely new - this is exactly the approach described in the book “
Creating Microservices ” by Sam Newman (I highly recommend). The embodiment of these ideas is found in many successful microservice architectures. This article is mainly devoted to the reuse of code in the domain, but similar principles apply to code that provides general connectivity and information sharing, since this does not contradict the principles outlined here.
Another question is possible: is it worth to worry about the binding of domain objects and connectivity with client libraries. As with the answer to our main question, the most important factor in this case is the influence of such details on the overall architecture. If we decide that the performance will increase, if we include the linking code in the client libraries, then we need to ensure that there will not be a strong link between the client services. Considering that connectivity in such architectures is usually provided with simple REST calls, or with the help of a message queue, I do not recommend putting such code in the client library, since it adds unnecessary dependencies but is not very profitable. If the code for connectivity has something special or too complex — for example, client certificates for performing SOAP requests, it may be advisable to attach an additional library. If you choose this path, always set the use of the client library as optional, not mandatory. Client services do not have to fully own the code (it is impossible to oblige the service provider to update the corresponding client libraries without fail).
EXAMPLE WITH SPRING BOOT
So, I explained the solution, and now I will demonstrate it in code. By the way, here is the opportunity to once again promote my favorite microservice library -
Spring Boot . The whole example can be downloaded from the
repository on Github , created specifically for this article.
Spring Boot allows you to develop microservices right off the bat - yes, I'm not exaggerating. If
Dropwizard seemed fast, then you are quite surprised at how comfortable it is to work with Spring Boot. In this example, we are developing a very simple
User
service that will return a simulated
User
JSON object. In the future, this service will be used by the notification service and the table service, in fact, building a variety of data views; however, in both cases, the service needs to understand the
User
object.
USER SERVICEIn
UserServiceApplication
will be the main method. Since this is Spring Boot, it also includes the built-in Tomcat server at startup:
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }
Indeed, it cannot be simpler! Spring Boot is a very categorical framework, so if defaults suit us, then almost nothing is necessary to manually type. However, you still have to change one thing: we are talking about the default port number. Let's see how this is done in the
application.properties
file:
server.port = 9001
Simple and beautiful. If you have ever written a REST service in Java, then you probably know that you need a
Controller
for this. If you are doing this for the first time - do not worry, writing controllers in Spring Boot is quite simple:
package com.example; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @RequestMapping("/user") public User getUser(@RequestParam(value="id", defaultValue="1") int id) { return new User(id); } }
So we simply allow the user to perform requests to the endpoint
/user?id=
, where the
id
can correspond to any user that interests us. Considering how simple these classes are - in fact, all the logic must lie in a particular
User
class. This class will generate procurement data and will be serialized using
Jackson (JSON library for Java):
package com.example; import java.util.ArrayList; import java.util.List; public class User { private final long id; private final String forename; private final String surname; private final String organisation; private final List<String> notifications; private final long points;
This is all the service needed to create User JSON. Since this is the first Spring Boot service we are considering, it will not hurt to look into the
.pom
file:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>user-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>user-service</name> <description>Demo user-service with Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.5.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
When calling the service, whose id is 10, we see the following JSON output:
CLIENT LIBRARYSuppose we have two services that use this API - a notification service and a personal account. In a realistic example, a User object could be much more complicated, and we could have more than two clients. The client library, a simple project called
user-client-libs
, consists of a single class:
@JsonIgnoreProperties(ignoreUnknown = true) public class UserView { private long id; private String forename; private String surname; private String organisation; private List<String> notifications; private long points; public UserView(){ } public long getId() { return id; } public String getForename() { return forename; } public String getSurname() { return surname; } public String getOrganisation() { return organisation; } public List<String> getNotifications() { return notifications; } public long getPoints() { return points; } }
As you can see, this class is simpler - there are no details associated with imitating users; there is no friends list, which is considered undesirable in the original class. We hide these details from customers. In such a lightweight implementation, the new fields that this API can return will also be ignored. Of course, in a realistic example, the client library could be much more complicated, which would save us time spent on typing the stencil code and help us better understand the relationships between the fields.
CUSTOMERSThis example shows the implementation of two separate client services. One is needed to create a "user account", and the other is for a "list of notifications." You can consider them as specialized microservices for working with user interface components.
Here is the personal account service controller:
import com.example.user.dto.UserView; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class UserDashboardController { @RequestMapping("/dashboard") public String getUser(@RequestParam(value="id", defaultValue="1") int id) { RestTemplate restTemplate = new RestTemplate(); UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class); return "USER DASHBOARD <br>" + "Welcome " + user.getForename() +" "+user.getSurname()+"<br>"+ "You have " +user.getPoints() + " points! Good job!<br>"+ "<br>"+ "<br>"+user.getOrganisation(); } }
And this is a personal notification service controller:
import com.example.user.dto.UserView; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class UserNotificationController { @RequestMapping("/notification") public String getUser(@RequestParam(value="id", defaultValue="1") int id) { RestTemplate restTemplate = new RestTemplate(); UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class); String response = "NOTIFICATIONS"; int number = 1; for(String notification : user.getNotifications()){ response += " Notification number "+(number++)+": "+notification; } return response; } }
As you can see, both clients are very simple, and the connection between them and the service is also trivial. Of course, we must add dependencies for both services to the .pom files.
<dependency> <groupId>com.example</groupId> <artifactId>user-client-libs</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
All that remains to be done in this example is to run all three services on ports 9001, 9002 and 9003 to see the output:
Personal Area:

Notifications:
CONCLUSIONI believe that this approach to design allows us to solve most of the problems with the reuse of code in the microservice architecture. It is understandable, avoids most of the shortcomings inherent in other approaches and simplifies the life of the developer. Moreover, it is a solution tested on real projects and well-proven.
In the example with Spring Boot, it is clearly demonstrated how convenient this approach is; besides, it turns out that microservices are much simpler than they might seem. If you want to study this project in more detail - look at my
Github and try to develop it.
Good luck with the development of microservices!
PS - from the authors of the translation:
→
Here is a book about Spring Boot.
→
Here is a book about microservices in Spring
Want some?