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 theuserService
, which is the interface that facilitates the communication between Account Service and User Service. If the user is not found, aResourceNotFound
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 providedAccountDto
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 theList<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 specifiedaccountId
.
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.
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!!!!😊