Microservices with Spring Boot : Synchronous Inter-Service Communication using Feign Client
In this article, we will see how two microservices developed using Spring Boot will synchronously communicate with each other using Spring's Feign Client.
Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load-balanced http client when using Feign.
We will build two services: a user service and a runner service. The runner service shall perform CRUD (Create Read Update Delete) operations with the user service which will store the data in an embedded H2 database.
Let's start:
Build the user service
Go to https://start.spring.io/
Note: For this article, we will use maven.
Add the following dependencies :
Spring Web
Lombok
Spring Data JPA
H2 Database
For this article, we are using Spring Boot version 2.7.8 and Java 11.
Click on Generate and open the project in an IDE (IntelliJ, Eclipse, VSCode, etc)
Create a User Entity
Create an entities package and inside it create a User.java class
User.java
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "users")
public class User
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
long id;
String firstName;
String lastName;
String email;
}
Create a JPA Repository for User
Create a package named repositories and create an interface for the user JPA repository.
UserRepository.java
import com.umang345.userservicesyncfeignclient.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
Add database properties
Add H2 Database properties and server port in application.properties file
server.port=8081
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2
Create custom exception
We will create a ResourceNotFoundException to deal with situations when the user that is requested is not present in the database.
We will create our exception classes in our exceptions package
ResourceNotFoundException.java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception
{
public ResourceNotFoundException(String message){
super(message);
}
public ResourceNotFoundException(){
super("The requested resource could not be found");
}
}
Create a custom error message
To handle the exception globally we will define a custom error message in our exceptions package.
ErrorMessage.java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
@Getter
@Setter
@Builder
public class ErrorMessage
{
private String message;
private String details;
}
Create a global exception handler
We will implement a global exception handler class that will handle our ResourceNotFoundException and also any generic exception.
GlobalExceptionHandler.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GlobalExceptionHandler
{
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> resourceNotFoundExceptionHandler(ResourceNotFoundException ex, WebRequest request){
ErrorMessage errorMessage = ErrorMessage
.builder()
.message(ex.getMessage())
.details(request.getDescription(false))
.build();
return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> globalExceptionHandler(Exception ex, WebRequest request){
ErrorMessage errorMessage = ErrorMessage
.builder()
.message(ex.getMessage())
.details(request.getDescription(false))
.build();
return new ResponseEntity<>(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Define the methods in the UserService interface
We will create a service layer over the JPA layer. Create a service package and add a UserService interface.
UserService.java
import com.umang345.userservicesyncfeignclient.entities.User;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface UserService
{
User createUser(User newUser);
User getUserById(long userId);
User updateUser(User user, long userId);
List<User> getAllUser();
void deleteUser(long userId);
}
Implement the UserService interface
We will add an implementation for the UserService interface.
UserServiceImpl
import com.umang345.userservicesyncfeignclient.entities.User;
import com.umang345.userservicesyncfeignclient.exceptions.ResourceNotFoundException;
import com.umang345.userservicesyncfeignclient.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User createUser(User newUser) {
User savedUser = userRepository.save(newUser);
return savedUser;
}
@Override
public User getUserById(long userId) {
User fetchedUser = null;
try {
fetchedUser = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id : "+userId));
} catch (ResourceNotFoundException e) {
e.printStackTrace();
}
return fetchedUser;
}
@Override
public User updateUser(User user, long userId) {
User currentUser = null;
try {
currentUser = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id : "+userId));
currentUser.setFirstName(user.getFirstName());
currentUser.setLastName(user.getLastName());
currentUser.setEmail(user.getEmail());
} catch (ResourceNotFoundException e) {
e.printStackTrace();
return null;
}
User updateUser = userRepository.save(currentUser);
return updateUser;
}
@Override
public List<User> getAllUser() {
List<User> users = userRepository.findAll();
return users;
}
@Override
public void deleteUser(long userId) {
User currentUser = null;
try {
currentUser = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id : "+userId));
} catch (ResourceNotFoundException e) {
e.printStackTrace();
}
userRepository.delete(currentUser);
}
}
Add the Controller for the User
We will implement a UserController that will expose the endpoints for the CRUD operations.
UserController.java
import com.umang345.userservicesyncfeignclient.entities.User;
import com.umang345.userservicesyncfeignclient.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/users")
public class UserController
{
@Autowired
private UserService userService;
@GetMapping("/{userId}")
public ResponseEntity<?> getUserById(@PathVariable Long userId)
{
User user = userService.getUserById(userId);
Map<String, Object> response = new HashMap<>();
if(user==null){
User nullUser = User.builder().id(0).firstName(null).lastName(null).email(null).build();
response.put("status", HttpStatus.NOT_FOUND.value());
response.put("data", nullUser);
return ResponseEntity.status(HttpStatus.OK).body(response);
}
response.put("status", HttpStatus.OK.value());
response.put("data", user);
return ResponseEntity.ok().body(response);
}
@GetMapping
public ResponseEntity<?> getAllUsers(){
List<User> users = userService.getAllUser();
Map<String, Object> response = new HashMap<>();
response.put("status", HttpStatus.OK.value());
response.put("data", users);
return ResponseEntity.ok().body(response);
}
@PostMapping
public ResponseEntity<?> createUser(@RequestBody User newUser) {
User createdUser = userService.createUser(newUser);
Map<String, Object> response = new HashMap<>();
response.put("status", HttpStatus.CREATED.value());
response.put("data", createdUser);
return ResponseEntity.ok().body(response);
}
@PutMapping("/{userId}")
public ResponseEntity<?> updateUser(@RequestBody User user, @PathVariable Long userId){
User updateUser = userService.updateUser(user,userId);
Map<String, Object> response = new HashMap<>();
if(updateUser==null){
User nullUser = User.builder().id(0).firstName(null).lastName(null).email(null).build();
response.put("status", HttpStatus.NOT_FOUND.value());
response.put("data", nullUser);
return ResponseEntity.status(HttpStatus.OK).body(response);
}
response.put("status", HttpStatus.OK.value());
response.put("data", updateUser);
return ResponseEntity.ok().body(response);
}
@DeleteMapping("/{userId}")
public ResponseEntity<?> deleteUser(@PathVariable Long userId)
{
userService.deleteUser(userId);
return ResponseEntity.ok().body("User deleted successfully with Id : "+userId);
}
}
pom.xml
The pom.xml for the user service must contain the following dependencies :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
With this, we complete our user service.
Build the Runner Service
Now we will build the runner service that is directly called by the client.
Go to https://start.spring.io/
Add the following dependencies :
Spring Web
Lombok
OpenFeign
For this article, we are using Spring Boot version 2.7.8 and Java 11.
Click on Generate and open the project in an IDE (IntelliJ, Eclipse, VSCode, etc)
Create the User entity
We will create the same user entity for the runner class by adding the database properties.
User.java
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User
{
long id;
String firstName;
String lastName;
String email;
}
Add an interface for the Feign Client
Create a package called feignClients and create an interface called RunnerFeignClient
RunnerFeignClient.java
import com.umang345.runnerservicesyncfeignclient.entities.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@FeignClient(name="RUNNER-SERVICE", url = "http://localhost:8081/users")
public interface RunnerFeignClient
{
@GetMapping
Map<String, Object> getAllUsers();
@GetMapping("/{userId}")
Map<String,Object> getUserById(@PathVariable long userId);
@PostMapping
Map<String,Object> createUser(User newUser);
@PutMapping("/{userId}")
Map<String,Object> updateUser(User user, @PathVariable Long userId);
@DeleteMapping("/{userId}")
void deleteUser(@PathVariable Long userId);
}
Add the Controller for the Runner Service
We will add the RunnerController that shall contain the endpoints for the client to call and the methods shall make a synchronous call to the user service to get the data.
RunnerController.java
import com.umang345.runnerservicesyncfeignclient.entities.User;
import com.umang345.runnerservicesyncfeignclient.feignClients.RunnerFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/simulate/users")
public class RunnerController
{
@Autowired
private RunnerFeignClient runnerFeignClient;
@GetMapping
public ResponseEntity<?> getAllUsers(){
Map<String,Object> response = runnerFeignClient.getAllUsers();
return ResponseEntity.ok().body(response.get("data"));
}
@GetMapping("/{userId}")
public ResponseEntity<?> getUserById(@PathVariable Long userId) {
Map<String,Object> response = new HashMap<>();
try {
response = runnerFeignClient.getUserById(userId);
if((Integer)response.get("status") != HttpStatus.OK.value())
{
throw new Exception("User not found with Id : "+userId);
}
return ResponseEntity.status(HttpStatus.OK).body(response.get("data"));
}
catch (Exception e){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}
@PostMapping
public ResponseEntity<?> createUser(@RequestBody User newUser){
Map<String, Object> response = new HashMap<>();
try {
response = runnerFeignClient.createUser(newUser);
if((Integer)response.get("status") != HttpStatus.CREATED.value())
{
throw new Exception("Error while creating user");
}
return ResponseEntity.status(HttpStatus.OK).body(response.get("data"));
}catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
@PutMapping("/{userId}")
public ResponseEntity<?> updateUser(@RequestBody User user, @PathVariable Long userId){
Map<String, Object> response = new HashMap<>();
try{
response = runnerFeignClient.updateUser(user,userId);
if((Integer)response.get("status") != HttpStatus.OK.value())
{
throw new Exception("User not found with Id : "+userId);
}
return ResponseEntity.status(HttpStatus.OK).body(response.get("data"));
}catch (Exception e){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}
@DeleteMapping("/{userId}")
public ResponseEntity<?> deleteUser(@PathVariable Long userId)
{
try {
runnerFeignClient.deleteUser(userId);
return ResponseEntity.status(HttpStatus.OK).body("User deleted successfully with id : "+userId);
}catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found with Id : "+userId);
}
}
}
Set the server port
We will set the server port in application.properties file
server.port=8080
pom.xml
pom.xml of the runner service should contain the following dependencies.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
This completes our runner service.
Note: Our user service is running on port 8081 and our runner service is running on port 8080.
We are using Postman for testing our services.
POST
We will create two users
{
"firstName" : "Umang",
"lastName" : "Agarwal",
"email" : "ua@test.com"
},
{
"firstName" : "John",
"lastName" : "Doe",
"email" : "jd@test.com"
}
GET ALL
Let's fetch all the users.
Get User By Id
Let's get the user with Id 2
PUT
Let's update the user with id 1
{
"id": 1,
"firstName" : "Umang",
"lastName" : "Agarwal",
"email" : "ua2@gmail.com"
}
Delete
Let's delete the user with id 2
We have tested all our endpoints here.
Find the source code of the project on GitHub.
Do star the repository to access the source code of all the articles.
I hope you found the article useful.
Let's connect :
Happy Coding :)