Microservices with Java — Part 1
Project setup, REST communication and Service Discovery
This is the first post of my microservices series, where I talk about my impressions of this architecture style using Java with Spring Boot.
The full series of articles are listed below:
- Part I: Project setup, REST communication and Service Discovery
- Part II: Fault tolerance and Configuration Server
- Part III: API Gateway and Centralized logs
- Part IV: Authentication and Authorization (it’s on my TODO list.. :-P)
The code implemented in each article is available on GitHub in a specific branch, and the final implementation is on the main branch.
In this article we will go through the initial project setup, understand the key concepts and implement the communication between microservices with service discovery and load balance.
The code of this article is available on GitHub in branch ‘ch-01’.
Microservices
In short, microservice is an architectural style that structures an application as a collection of services that are independent, loosely coupled, organized around business capabilities and owned by a small team.[1]
According to Martin Fowler,
“the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API.”[2]
This kind of architecture enables the reliable delivery of large, complex applications, and also enables an organization to evolve its technology stack, since each microservice is independent and can have its own set of technologies.
From my personal perspective, this independence of technology allow us to apply the proper tech stack for each service, which is a huge advantage of this type of architecture.
There is a debate about how data should be persisted between services, but in my opinion each microservice has to have its own database, storing data of its own context.
This article assumes basic knowledge about Java, Spring framework and microservices, and will focus in the implementation of its concepts.
Project setup
Let’s learn by building a system using microservices to demonstrate its features.
Initially we are going to implement three REST API applications using Spring Boot, as described below:
- product-catalog microservice: responsible for storing and managing products information;
- user-info microservice: responsible for storing and managing user information;
- shopping-cart microservice: responsible for orchestrate user purchase of products, retrieving data from the other microservices.
All services will be REST APIs providing operations on its domain, and will store data in a in-memory H2 datase using JPA.
So, let’s create the project using Spring Initializr page (using Maven + Java 11). To make it simple, for the product-catalog and user-info services, we will use ‘REST Respositories’, that exposes CRUD operations direct from Spring Data repositories. So it’s necessary to include these Spring dependencies. We will also include the Lombok dependency to avoid typing boiler-plate code:
The product and user microservices setup are very similar, and you can create the user-info project with the same dependencies shown above, but for shopping-cart microservice we will need some additional dependencies for REST requests, and H2 database dependency will not be necessary. We will add the MapStruct dependency too for automatic mapping attributes between classes.
As usual, we need to download the generated zip file and open all the projects in our favorite IDE (I’ll be using IntelliJ IDEA).
Next step, let’s edit the ‘application.yml’ file (or ‘application.properties’, if you like), so to set proper application names, database configs and different port numbers to run them all simultaneously. I chose to set them to run on ports 8081, 8082 and 8083, but feel free to choose different ports.
As an example, here is the product-catalog/application.yml file content:
spring:
application:
name: PRODUCT-CATALOG-SERVICE
datasource:
url: jdbc:h2:mem:productdb
driverClassName: org.h2.Driver
username: admin
password: admin
database-platform: org.hibernate.dialect.H2Dialect
h2:
console:
enabled: true
server:
port: 8081
Click in the links below to see the complete maven (pom.xml) and configuration files on GitHub (ignore the ‘eureka’ settings for now):
- product-catalog project: pom.xml and application.yml
- user-info project: pom.xml and application.yml
- shopping-cart project: pom.xml and application.yml
At this point, we should have three proper configured applications that does nothing! :-P
Implementing the Product Catalog Microservice
We are going to implement a structure like this, which will be described step-by-step:
Let’s create the JPA entity class that represents a persistent product:
@NoArgsConstructor @AllArgsConstructor
@Getter @Setter
@Entity
@Table(name = "PRODUCTS")
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String description;
private String brand;
private BigDecimal price;
}
Using Spring Data, we can create the repository interface for product entities, and let’s make it a ‘REST Resource Repository’:
@RepositoryRestResource(collectionResourceRel = "product", path = "product")
public interface ProductRepository extends JpaRepository<ProductEntity, Long> { }
To add entity ‘id’ in responses, we need to register a configuration Spring bean, that I chose to add in the main class:
@Component
class ExposeEntityIdRestMvcConfiguration implements RepositoryRestConfigurer {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
config.exposeIdsFor(ProductEntity.class);
}
}
To make it easier to test, we can populate our local database using a initialization SQL script (‘data.sql’), which has to be stored at ‘src/main/resources’ directory. The script for products is available here.
And after launching the application (by its main method in ProductCatalogApplication class), we can test the GET (find) service using Postman for example:
Implementing the User Information Microservice
Likewise the product service, we can create the structure of the user-info microservice project as shown below:
And also the JPA entity class:
@NoArgsConstructor @AllArgsConstructor
@Getter @Setter
@Entity
@Table(name = "USERS")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
private LocalDate birthdate;
private String address;
}
And then the repository interface for User entities, again as a REST Repository:
@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UserRepository extends JpaRepository<UserEntity, Long> { }
The initialization SQL script (‘data.sql’) for users is available here.
And now we can launch the application (by its main method in UserInfoApplication class), and test the GET (find) service using Postman for example:
Implementing the Shopping-Cart Microservice
The structure of the shopping-cart project is a little bit bigger, and will be like this:
Let’s implement the model/domain classes, where a Cart has a list of product items and a reference to the user of the purchase:
@Data @NoArgsConstructor @AllArgsConstructor
public class Cart {
private String id;
private UserInfo user;
private List<Item> items;
private BigDecimal totalPrice;
public Cart(String id, UserInfo user, List<Item> items) {
this(id, user, items, BigDecimal.ZERO);
calculateTotalPrice();
}
private void calculateTotalPrice() {
if (items == null) return;
totalPrice = items.stream()
.map(item -> item.getProduct().getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal getTotalPrice() {
calculateTotalPrice();
return totalPrice;
}
}
@Data @NoArgsConstructor @AllArgsConstructor
public class UserInfo {
private Long id;
private String name;
private String email;
private String address;
}
@Data @NoArgsConstructor @AllArgsConstructor
public class ProductOverview {
private Long id;
private String name;
private BigDecimal price;
public BigDecimal getPrice() {
if (price == null)
price = BigDecimal.ZERO;
return price;
}
}
@Data @NoArgsConstructor @AllArgsConstructor
public class Item {
private ProductOverview product;
private int quantity;
}
I like to decouple model classes from request/response classes, as well entity classes. So let’s create request/response DTOs:
@AllArgsConstructor @NoArgsConstructor
@Getter @Setter
public class ShoppingCartRequestDTO {
private Long userId;
private List<ItemDTO> items;
}@AllArgsConstructor @NoArgsConstructor
@Getter @Setter
public class ItemDTO {
private Long productId;
private String productName;
private int quantity;
} @AllArgsConstructor @NoArgsConstructor
@Getter @Setter
public class ShoppingCartResponseDTO {
private String id;
private Long userId;
private String userName;
private List<ItemDTO> items;
private BigDecimal totalPrice;
}
We can automatically generate a mapper class using MapStruct interfaces to instantiate classes from/to model and DTO:
@Mapper(componentModel="spring")
public interface CartMapper {
@Mapping(source = "userId", target = "user.id")
Cart toModel(ShoppingCartRequestDTO dto);
@Mapping(source = "user.id", target = "userId")
@Mapping(source = "user.name", target = "userName")
ShoppingCartResponseDTO toResponseDTO(Cart model);
@Mapping(source = "productId", target = "product.id")
@Mapping(source = "productName", target = "product.name")
Item toModel(ItemDTO dto);
@Mapping(source = "productId", target = "product.id")
@Mapping(source = "productName", target = "product.name")
List<Item> toModel(List<ItemDTO> dto);
@Mapping(source = "product.id", target = "productId")
@Mapping(source = "product.name", target = "productName")
ItemDTO toDTO(Item model);
@Mapping(source = "product.id", target = "productId")
@Mapping(source = "product.name", target = "productName")
List<ItemDTO> toDTO(List<Item> model);
}
The business logic will be implemented at a service class. For the purpose of this article, we will keep it as simple as possible (no validation involved):
@Service
@AllArgsConstructor
public class ShoppingCartService { private IntegrationService integrationService;
public Cart purchase(Cart shoppingCart) {
var uuid = UUID.randomUUID().toString();
shoppingCart.setId(uuid);
var user = integrationService.getRemoteUserInfo(shoppingCart.getUser().getId());
shoppingCart.setUser(user);
var items = integrationService.getRemoteProductItemsInfo(shoppingCart.getItems());
shoppingCart.setItems(items);
integrationService.submitToBilling(shoppingCart);
integrationService.notifyToDelivery(shoppingCart);
integrationService.askForUserReview(shoppingCart);
return shoppingCart;
}
}
For now, let’s ignore the ‘IntegrationService’ dependency, which will be the interface to external communication to be seen later in this article.
Now we can implement the REST controller class to support operations over HTTP, where we will have a single endpoint to submit the purchase:
@RestController
@RequestMapping("api/cart")
@AllArgsConstructor
public class ShoppingCartController {
private ShoppingCartService service;
private CartMapper mapper;
@PostMapping
public ResponseEntity<ShoppingCartResponseDTO> submit(@RequestBody ShoppingCartRequestDTO requestDTO) {
var cart = mapper.toModel(requestDTO);
cart = service.purchase(cart);
var responseDTO = mapper.toResponseDTO(cart);
return ResponseEntity.created(URI.create(responseDTO.getId())).body(responseDTO);
}
}
And now we are at the point where we can implement the integration between the shopping-cart microservice to consume data from product and users microservices.
Integration between microservices
It is possible to implement the REST communication in many ways, and in this article we are going to implement three options: using RestTemplate, Spring WebClient and Feign.
The code of this project on GitHub will contain all the three implementations for demonstration, but I’ll leave the Feign implementation uncommented because it seems to me the cleaner way to do the job.
By adopting any implementation option, the POST request to shopping-cart endpoint should result in a identified and processed purchase as illustrated below:
All integration code will be isolated in the ‘IntegrationService’ class.
Option 01: using RestTemplate
RestTemplate is a very common solution to perform HTTP requests, and it is quite simple. We can instantiate a client just by using the new keyword: new RestTemplate().
Here, we are going to create a Spring Bean, to reuse the created instance in the application. This bean was added at the application main class:
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
And now we can inject this instance at our integration class.
The initial implementation using hardcoded URLs could be like shown below. First I had implemented one method to call each service, but it could be improved to a generic method that I liked most. I kept the initial methods here (marked as deprecated) for demonstration purposes.
// constants
private final String USER_BASE_URL = "http://localhost:8082/api/user/";
private final String PRODUCT_BASE_URL = "http://localhost:8081/api/product/"; // Generic implementation for Rest Template call
private <T> T fetchDataWithRestTemplate(String url, Long id, Class<T> clazz) {
return restTemplate.getForObject(url + id, clazz);
}
@Deprecated // replaced by generic method
private UserInfo fetchUserWithRestTemplate(Long userId) {
return restTemplate.getForObject("http://" + USER_BASE_URL + userId, UserInfo.class);
}
@Deprecated // replaced by generic method
private ProductOverview fetchProductWithRestTemplate(Long prodId) {
return restTemplate.getForObject("http://" + PRODUCT_BASE_URL + prodId, ProductOverview.class);
}
So the first version of the integration service class would be like this:
@Service
@AllArgsConstructor
public class IntegrationService {
private final RestTemplate restTemplate; // injected bean
private final String USER_BASE_URL = "http://localhost:8082/api/user/";
private final String PRODUCT_BASE_URL = "http://localhost:8081/api/product/";
public UserInfo getRemoteUserInfo(Long userId) {
var user = fetchDataWithRestTemplate("http://" + USER_BASE_URL, userId, UserInfo.class);
return user;
}
public List<Item> getRemoteProductItemsInfo(List<Item> items) {
items.forEach(item -> {
var product = fetchDataWithRestTemplate("http://" + PRODUCT_BASE_URL, item.getProduct().getId(), ProductOverview.class);
item.setProduct(product);
});
return items;
}
// Generic implementation for Rest Template call
private <T> T fetchDataWithRestTemplate(String url, Long id, Class<T> clazz) {
return restTemplate.getForObject(url + id, clazz);
}
@Deprecated // replaced by generic method
private UserInfo fetchUserWithRestTemplate(Long userId) {
return restTemplate.getForObject("http://" + USER_BASE_URL + userId, UserInfo.class);
}
@Deprecated // replaced by generic method
private ProductOverview fetchProductWithRestTemplate(Long productId) {
return restTemplate.getForObject("http://" + PRODUCT_BASE_URL + productId, ProductOverview.class);
}
}
Option 02: using Spring WebClient
Spring WebClient is part of Spring WebFlux, which in turn is part of the React stack solution of the Spring framework. It provides fluent interfaces for data streaming. It should be the successor of the RestTemplate, although RestTemplate is still very used in production nowadays.
We can get an instance of a WebClient by its builder, that we will set as a Bean too in the main class:
@Bean
public WebClient.Builder getWeClientBuilder() {
return WebClient.builder();
}
Likewise the previous example, a generic implementation using WebFlux would be like this:
@Service
@AllArgsConstructor
public class IntegrationService {
private final WebClient.Builder webClientBuilder; // injected
private final String USER_BASE_URL = "http://localhost:8082/api/user/";
private final String PRODUCT_BASE_URL = "http://localhost:8081/api/product/";
public UserInfo getRemoteUserInfo(Long userId) {
var user = fetchDataWithWebClient("http://" + USER_BASE_URL, userId, UserInfo.class);
return user;
}
public List<Item> getRemoteProductItemsInfo(List<Item> items) {
items.forEach(item -> {
var product = fetchDataWithWebClient("http://" + PRODUCT_BASE_URL, item.getProduct().getId(), ProductOverview.class);
item.setProduct(product);
});
return items;
}
// Generic implementation for WebClient
private <T> T fetchDataWithWebClient(String url, Long id, Class<T> clazz) {
return webClientBuilder.build().get()
.uri(url + id).retrieve()
.bodyToMono(clazz)
.block(); // makes sync call
}
}
Option 03: Feign
Feign is a library to represent HTTP calls through annotated interfaces. It seems to me very like the Android Retrofit library, and in fact apparently it was inspired in retrofit indeed .
So, to use this lib, let’s create an interface for each external service accessed (products and users). Notice the ‘FeignClient’ annotation that does the url bind to the service, and that the method at interface matches the signature to the endpoint:
@FeignClient(url = "http://localhost:8081/api/product/", name = "ProductCatalogService")
public interface ProductFeignClient {
@GetMapping("{id}")
ProductOverview findById(@PathVariable("id") Long id);
}@FeignClient(url = "http://localhost:8082/api/user/", name = "UserInformationService")
public interface UserFeignClient {
@GetMapping("/api/user/{id}")
UserInfo findById(@PathVariable("id") Long id);
}
And let’s modify the IntegrationService class to inject these Feign interfaces as attributes:
@Service
@AllArgsConstructor
public class IntegrationService { private final UserFeignClient userFeignClient;
private final ProductFeignClient productFeignClient;
public UserInfo getRemoteUserInfo(Long userId) {
var user = userFeignClient.findById(userId);
return user;
}
public List<Item> getRemoteProductItemsInfo(List<Item> items) {
items.forEach(item -> {
var product = productFeignClient.findById(item.getProduct().getId());
item.setProduct(product);
});
return items;
}
}
Finally, we just need to enable Feign clients using ‘EnableFeignClients’ annotation in shopping-cart main class.
Service Discovery
Even though this application works, there are obvious problems like the hardcoded URLs in the Integration class code. What if there were more than one instance of the dependent services? What if the URL changes? How to scale this application and load balance the requests?
To solve these issues, we can implement the Service Discovery pattern, where we create a Discovery Server which each instance of the microservices register itself, and when some service needs to make a request to other, first it requests the available instance location from this Discovery Server (“client side service discovery”):
The technology to achieve this in this article will be Eureka, which is part of Spring Cloud Netflix solution.
So, what we need to do is create a new project with Eureka Server dependency, and then set some configurations in others microservices to make each of the them register themselves at this discovery server.
After openning the project in IDE, we need to enable eureka server in the app main class. Notice the ‘EnableEurekaServer’ annotation:
@SpringBootApplication
@EnableEurekaServer
public class ServiceDiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceDiscoveryServerApplication.class, args);
}
}
And for configuration application.yml file, let’s use the Eureka server default port and set it to not register itself as a ‘discoverable’ service:
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
And that’s it! After launch the application we have a discovery server running, but first let’s do the client configuration to see it in action.
So, in the products, users and shopping-cart projects, let’s add the eureka client dependency in pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
And in the application.yml of each project, let’s add the eureka client configs to reach the discovery server:
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
And finally, let’s enable eureka client in each application’s main class using the annotation ‘EnableEurekaClient’. To automatically get load balance between available instances of services, we just need to add the proper annotation in the RestTemplate bean. The Feign library integrated with eureka client already does the load balanced.
The annotated main classes are shown below:
@SpringBootApplication
@EnableEurekaClient
public class ProductCatalogApplication {
public static void main(String[] args) {
SpringApplication.run(ProductCatalogApplication.class, args);
}
// ommited Rest Repository Bean
}
@SpringBootApplication
@EnableEurekaClient
public class UserInfoApplication {
public static void main(String[] args) {
SpringApplication.run(UserInfoApplication.class, args);
}
// ommited Rest Repository Bean
}
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ShoppingCartApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingCartApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
By running the Discovery Server and then the other three microservices, we can see the registered microservices at eureka’s web page at http://localhost:8761/ :
Now that the Eureka server and clients are properly configured, we can adjust the Feign clients to use registered services names instead of hardcoded URLs:
@FeignClient("PRODUCT-CATALOG-SERVICE")
public interface ProductFeignClient {
@GetMapping("/api/product/{id}")
ProductOverview findById(@PathVariable("id") Long id);
}@FeignClient("USER-INFORMATION-SERVICE")
public interface UserFeignClient {
@GetMapping("/api/user/{id}")
UserInfo findById(@PathVariable("id") Long id);
}
And adjust the IntegrationService class to also use the application names that are registered in Eureka Server:
@Service
@AllArgsConstructor
public class IntegrationService {
private final RestTemplate restTemplate;
private final WebClient.Builder webClientBuilder;
private final UserFeignClient userFeignClient;
private final ProductFeignClient productFeignClient;
private final String USER_SERVICE_NAME = "USER-INFORMATION-SERVICE";
private final String PRODUCT_SERVICE_NAME = "PRODUCT-CATALOG-SERVICE";
public UserInfo getRemoteUserInfo(Long userId) {
var user = userFeignClient.findById(userId);
return user;
}
public List<Item> getRemoteProductItemsInfo(List<Item> items) {
items.forEach(item -> {
var product = productFeignClient.findById(item.getProduct().getId());
item.setProduct(product);
});
return items;
}
// Generic implementation for Rest Template call
private <T> T fetchDataWithRestTemplate(String url, Long id, Class<T> clazz) {
return restTemplate.getForObject(url + id, clazz);
}
// Generic implementation for WebClient
private <T> T fetchDataWithWebClient(String url, Long id, Class<T> clazz) {
return webClientBuilder.build().get()
.uri(url + id).retrieve()
.bodyToMono(clazz)
.block(); // makes sync
}
@Deprecated // replaced by generic method
private UserInfo fetchUserWithRestTemplate(Long userId) {
return restTemplate.getForObject("http://" + USER_SERVICE_NAME + "/api/user/" + userId, UserInfo.class);
}
@Deprecated // replaced by generic method
private ProductOverview fetchProductWithRestTemplate(Long productId) {
return restTemplate.getForObject("http://" + PRODUCT_SERVICE_NAME + "/api/product/" + productId, ProductOverview.class);
}
}
And after launching the Discovery Server and all the others microservices, we should have the same result/response when submit a POST request to the shopping-cart endpoint. But now we have the advantage of not to be attached to hardcoded URLs, and we have load balanced client services.
It is possible to run other instances of products and users (in different ports, of course), and Eureka will do the job of registering them and balance the request calls between them.
Again, the full source-code implemented in this post is available on GitHub in the ‘ch-01’ branch.
There is a lot more involving microservices architecture, and I wrote about some other aspects of it in next posts of this series os articles.
Resources and further readings: