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

application.properties

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

application.properties

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 :)