Mastering Microservices: Inter - Service Communication using Spring Cloud Feign Client  Part- 2

Mastering Microservices: Inter - Service Communication using Spring Cloud Feign Client Part- 2

🚀 Welcome to our Blog on Implementing the Spring Cloud OpenFeign Client for the Microservice Banking Application! 🌐📈

Embark on a journey to master the intricacies of using Spring Cloud OpenFeign Client within our microservices-driven banking system. Whether you're a seasoned developer or a tech enthusiast, join us for hands-on guidance and an in-depth exploration of constructing a robust microservices banking application.

If you are new to the Spring Cloud OpenFeign Client, navigate to the Spring Boot Tutorial: Spring Cloud Feign Client article and delve into its contents for exploration🤯.

Pre - work

Before commencing, we must establish another service functioning as a number generator. This service is pivotal for generating account numbers in sequence, a functionality utilized in the account service during the account creation process which we created in the Mastering Microservices: Implemenatation of Account Service.

So, I have explained how we would be creating the Sequence Generator service in the article "Mastering Microservices: Implementation of Sequence Generator. Please refer to it for detailed insights.

Development

Now, move to your favorite IDE or to the Spring Initializer and get the following dependencies add them to our Account Service project that we created in the Mastering Microservices: Implemenatation of Account Service article.

All the modifications we are implementing are for the Account Service application, so please ensure this focus throughout the process.

Now, what are we gonna use?

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

We will be having some new layers to implement the Spring Cloud OpenFeign client.

Model Layer

Now, within the model layer, we will generate three classes named SequenceDto, TransactionResponse, and UserDto in the package labeled model.dto.external. These classes will be employed while retrieving data from other services.

SequenceDto.java

package org.training.account.service.model.dto.external;

import lombok.Data;

@Data
public class SequenceDto {

    private long sequenceId;

    private long accountNumber;
}

TransactionResponse.java

package org.training.account.service.model.dto.external;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TransactionResponse {

    private String referenceId;

    private String accountId;

    private String transactionType;

    private BigDecimal amount;

    private LocalDateTime localDateTime;

    private String transactionStatus;

    private String comments;
}

UserDto.java

package org.training.account.service.model.dto.external;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDto {

    private Long userId;

    private String firstName;

    private String lastName;

    private String emailId;

    private String password;

    private String identificationNumber;

    private String authId;
}

External Layer

We will be crafting three interfaces, namely SequenceService, TransactionService, and UserService, within the package named external. These interfaces will facilitate communication with the respective services.

SequenceService.java

package org.training.account.service.external;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.training.account.service.model.dto.external.SequenceDto;

@FeignClient(name = "sequence-generator")
public interface SequenceService {

    @PostMapping("/sequence")
    SequenceDto generateAccountNumber();
}

TransactionService.java

package org.training.account.service.external;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.training.account.service.configuration.FeignClientConfiguration;
import org.training.account.service.model.dto.external.TransactionResponse;

import java.util.List;

@FeignClient(name = "transaction-service", configuration = FeignClientConfiguration.class)
public interface TransactionService {

    @GetMapping("/transactions")
    List<TransactionResponse> getTransactionsFromAccountId(@RequestParam String accountId);
}

UserService.java

package org.training.account.service.external;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.training.account.service.configuration.FeignClientConfiguration;
import org.training.account.service.model.dto.external.UserDto;

@FeignClient(name = "user-service", configuration = FeignClientConfiguration.class)
public interface UserService {

    @GetMapping("/api/users/{userId}")
    ResponseEntity<UserDto> readUserById(@PathVariable Long userId);
}

Service Layer

In this layer, we will be updating the code to utilize the readUserById(Long userId) method, which was commented in the previous article, Mastering Microservices: Implementation of Account Service in the implementation of methods.

public Response createAccount(AccountDto accountDto) {

        ResponseEntity<UserDto> user = userService.readUserById(accountDto.getUserId());
        if (Objects.isNull(user.getBody())) {
            throw new ResourceNotFound("user not found on the server");
        }

        accountRepository.findAccountByUserIdAndAccountType(accountDto.getUserId(), AccountType.valueOf(accountDto.getAccountType()))
                .ifPresent(account -> {
                    log.error("Account already exists on the server");
                    throw new ResourceConflict("Account already exists on the server");
                });

        Account account = accountMapper.convertToEntity(accountDto);
        account.setAccountNumber(ACC_PREFIX + String.format("%07d",sequenceService.generateAccountNumber().getAccountNumber()));
        account.setAccountStatus(AccountStatus.PENDING);
        account.setAvailableBalance(BigDecimal.valueOf(0));
        account.setAccountType(AccountType.valueOf(accountDto.getAccountType()));
        accountRepository.save(account);
        return Response.builder()
                .responseCode(success)
                .message(" Account created successfully").build();
    }

In the implementation of the createAccount(AccountDto accountDto) method, we had previously commented out some lines. Now, we simply need to remove the comments, ensuring the code aligns with the example provided above.

  • The method takes an AccountDto object as a parameter, representing the account details to be created.

  • It first checks if the associated user exists by calling the readUserById method from the userService , which is the interface that facilitates the communication between Account Service and User Service. If the user is not found, a ResourceNotFound exception is thrown.

  • The method then checks if an account of the specified type already exists for the given user. If so, a ResourceConflict exception is thrown.

  • If the checks pass, a new Account entity is created using the provided AccountDto and some default or calculated values.

  • The account number is generated using a prefix and a formatted sequence obtained from the sequenceService , this facilitates communication between Account Service and Sequence Service.

  • The account status is set to 'PENDING,' available balance to zero, and the account type is set from the AccountDto.

  • The new account is saved to the repository using accountRepository.save(account).

  • Finally, a Response object is built indicating a successful account creation with a success code and a corresponding message.

Now, we will be implementing the getTransactionsFromAccountId(String accountId) method which we had not implemented in the Account Service.

Add the below code to the implementation class of the AccountService interface. Additionally, confirm if you want the List<TransactionResponse> getTransactionsFromAccountId(String accountId) method to be added to the interface, and if so, I'll include it accordingly.

@Override
public List<TransactionResponse> getTransactionsFromAccountId(String accountId) {
    return transactionService.getTransactionsFromAccountId(accountId);
}
  • The getTransactionsFromAccountId method is overridden, from an interface.

  • It delegates the responsibility of retrieving transactions to the transactionService.

  • The method returns a list of TransactionResponse objects, indicating transactions associated with the specified accountId.

The entire code in the AccountService interface and it's implementation is given below.

AccountService.java

package org.training.account.service.service;

import org.training.account.service.model.dto.AccountDto;
import org.training.account.service.model.dto.AccountStatusUpdate;
import org.training.account.service.model.dto.response.Response;
import org.training.account.service.model.dto.external.TransactionResponse;

import java.util.List;

public interface AccountService {

    Response createAccount(AccountDto accountDto);

    Response updateStatus(String accountNumber, AccountStatusUpdate accountUpdate);

    AccountDto readAccountByAccountNumber(String accountNumber);

    Response updateAccount(String accountNumber, AccountDto accountDto);

    String getBalance(String accountNumber);

    List<TransactionResponse> getTransactionsFromAccountId(String accountId);

    Response closeAccount(String accountNumber);

    AccountDto readAccountByUserId(Long userId);
}

AccountServiceImpl.java

package org.training.account.service.service.implementation;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.training.account.service.exception.*;
import org.training.account.service.external.SequenceService;
import org.training.account.service.external.TransactionService;
import org.training.account.service.external.UserService;
import org.training.account.service.model.AccountStatus;
import org.training.account.service.model.AccountType;
import org.training.account.service.model.dto.AccountDto;
import org.training.account.service.model.dto.AccountStatusUpdate;
import org.training.account.service.model.dto.external.UserDto;
import org.training.account.service.model.dto.response.Response;
import org.training.account.service.model.entity.Account;
import org.training.account.service.model.mapper.AccountMapper;
import org.training.account.service.model.dto.external.TransactionResponse;
import org.training.account.service.repository.AccountRepository;
import org.training.account.service.service.AccountService;

import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;

import static org.training.account.service.model.Constants.ACC_PREFIX;

@Slf4j
@Service
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {

    private final UserService userService;
    private final AccountRepository accountRepository;
    private final SequenceService sequenceService;
    private final TransactionService transactionService;

    private final AccountMapper accountMapper = new AccountMapper();


    @Value("${spring.application.ok}")
    private String success;

    @Override
    public Response createAccount(AccountDto accountDto) {

        ResponseEntity<UserDto> user = userService.readUserById(accountDto.getUserId());
        if (Objects.isNull(user.getBody())) {
            throw new ResourceNotFound("user not found on the server");
        }

        accountRepository.findAccountByUserIdAndAccountType(accountDto.getUserId(), AccountType.valueOf(accountDto.getAccountType()))
                .ifPresent(account -> {
                    log.error("Account already exists on the server");
                    throw new ResourceConflict("Account already exists on the server");
                });

        Account account = accountMapper.convertToEntity(accountDto);
        account.setAccountNumber(ACC_PREFIX + String.format("%07d",sequenceService.generateAccountNumber().getAccountNumber()));
        account.setAccountStatus(AccountStatus.PENDING);
        account.setAvailableBalance(BigDecimal.valueOf(0));
        account.setAccountType(AccountType.valueOf(accountDto.getAccountType()));
        accountRepository.save(account);
        return Response.builder()
                .responseCode(success)
                .message(" Account created successfully").build();
    }

    @Override
    public Response updateStatus(String accountNumber, AccountStatusUpdate accountUpdate) {

        return accountRepository.findAccountByAccountNumber(accountNumber)
                .map(account -> {
                    if(account.getAccountStatus().equals(AccountStatus.ACTIVE)){
                        throw new AccountStatusException("Account is inactive/closed");
                    }
                    if(account.getAvailableBalance().compareTo(BigDecimal.ZERO) < 0 || account.getAvailableBalance().compareTo(BigDecimal.valueOf(1000)) < 0){
                        throw new InSufficientFunds("Minimum balance of Rs.1000 is required");
                    }
                    account.setAccountStatus(accountUpdate.getAccountStatus());
                    accountRepository.save(account);
                    return Response.builder().message("Account updated successfully").responseCode(success).build();
                }).orElseThrow(() -> new ResourceNotFound("Account not on the server"));

    }

    @Override
    public AccountDto readAccountByAccountNumber(String accountNumber) {

        return accountRepository.findAccountByAccountNumber(accountNumber)
                .map(account -> {
                    AccountDto accountDto = accountMapper.convertToDto(account);
                    accountDto.setAccountType(account.getAccountType().toString());
                    accountDto.setAccountStatus(account.getAccountStatus().toString());
                    return accountDto;
                })
                .orElseThrow(ResourceNotFound::new);
    }

    @Override
    public Response updateAccount(String accountNumber, AccountDto accountDto) {

        return accountRepository.findAccountByAccountNumber(accountDto.getAccountNumber())
                .map(account -> {
                    System.out.println(accountDto);
                    BeanUtils.copyProperties(accountDto, account);
                    accountRepository.save(account);
                    return Response.builder()
                            .responseCode(success)
                            .message("Account updated successfully").build();
                }).orElseThrow(() -> new ResourceNotFound("Account not found on the server"));
    }

    @Override
    public String getBalance(String accountNumber) {

        return accountRepository.findAccountByAccountNumber(accountNumber)
                .map(account -> account.getAvailableBalance().toString())
                .orElseThrow(ResourceNotFound::new);
    }

    @Override
    public List<TransactionResponse> getTransactionsFromAccountId(String accountId) {

        return transactionService.getTransactionsFromAccountId(accountId);
    }

    @Override
    public Response closeAccount(String accountNumber) {

        return accountRepository.findAccountByAccountNumber(accountNumber)
                .map(account -> {
                    if(BigDecimal.valueOf(Double.parseDouble(getBalance(accountNumber))).compareTo(BigDecimal.ZERO) != 0) {
                        throw new AccountClosingException("Balance should be zero");
                    }
                    account.setAccountStatus(AccountStatus.CLOSED);
                    return Response.builder()
                            .message("Account closed successfully").message(success)
                            .build();
                }).orElseThrow(ResourceNotFound::new);

    }

    @Override
    public AccountDto readAccountByUserId(Long userId) {

        return accountRepository.findAccountByUserId(userId)
                .map(account ->{
                    if(!account.getAccountStatus().equals(AccountStatus.ACTIVE)){
                        throw new AccountStatusException("Account is inactive/closed");
                    }
                    AccountDto accountDto = accountMapper.convertToDto(account);
                    accountDto.setAccountStatus(account.getAccountStatus().toString());
                    accountDto.setAccountType(account.getAccountType().toString());
                    return accountDto;
                }).orElseThrow(ResourceNotFound::new);
    }
}

Controller Layer

Now we, will expose the method getTransactionsFromAccountId(String accountId) as API endpoint.

@GetMapping("/{accountId}/transactions")
public ResponseEntity<List<TransactionResponse>> getTransactionsFromAccountId(@PathVariable String accountId) {
    return ResponseEntity.ok(accountService.getTransactionsFromAccountId(accountId));
}

The entire code for the controller class is given below.

AccountController.java

package org.training.account.service.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.training.account.service.model.dto.AccountDto;
import org.training.account.service.model.dto.AccountStatusUpdate;
import org.training.account.service.model.dto.response.Response;
import org.training.account.service.model.dto.external.TransactionResponse;
import org.training.account.service.service.AccountService;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/accounts")
public class AccountController {

    private final AccountService accountService;

    @PostMapping
    public ResponseEntity<Response> createAccount(@RequestBody AccountDto accountDto) {
        return new ResponseEntity<>(accountService.createAccount(accountDto), HttpStatus.CREATED);
    }

    @PatchMapping
    public ResponseEntity<Response> updateAccountStatus(@RequestParam String accountNumber,@RequestBody AccountStatusUpdate accountStatusUpdate) {
        return ResponseEntity.ok(accountService.updateStatus(accountNumber, accountStatusUpdate));
    }

    @GetMapping
    public ResponseEntity<AccountDto> readByAccountNumber(@RequestParam String accountNumber) {
        return ResponseEntity.ok(accountService.readAccountByAccountNumber(accountNumber));
    }

    @PutMapping
    public ResponseEntity<Response> updateAccount(@RequestParam String accountNumber, @RequestBody AccountDto accountDto) {
        return ResponseEntity.ok(accountService.updateAccount(accountNumber, accountDto));
    }

    @GetMapping("/balance")
    public ResponseEntity<String> accountBalance(@RequestParam String accountNumber) {
        return ResponseEntity.ok(accountService.getBalance(accountNumber));
    }

    @GetMapping("/{accountId}/transactions")
    public ResponseEntity<List<TransactionResponse>> getTransactionsFromAccountId(@PathVariable String accountId) {
        return ResponseEntity.ok(accountService.getTransactionsFromAccountId(accountId));
    }

    @PutMapping("/closure")
    public ResponseEntity<Response> closeAccount(@RequestParam String accountNumber) {
        return ResponseEntity.ok(accountService.closeAccount(accountNumber));
    }

    @GetMapping("/{userId}")
    public ResponseEntity<AccountDto> readAccountByUserId(@PathVariable Long userId){
        return ResponseEntity.ok(accountService.readAccountByUserId(userId));
    }
}

Note: Please don't try to run the application, since we need to add exception handling configuration.

Postman Collection

I'm using Postman to test the APIs, and I will be attaching the Postman collection below so that you can go through it.

Run In Postman

Conclusion

Thanks for reading our latest article on Mastering Microservices: Inter - Service Communication using Spring Cloud Feign Client Part- 2 with practical usage.

You can get source code for this tutorial from our GitHub repository.

Happy Coding!!!!😊

Did you find this article valuable?

Support Karthik Kulkarni by becoming a sponsor. Any amount is appreciated!