Mastering Microservices: Inter - Service Communication using Spring Cloud Feign Client Part- 3
π 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π€―.
Development
Now, move to your favorite IDE or to the Spring Initializer and get the following dependencies add them to our Fund Transfer Service project that we created in the Mastering Microservices: Implemenatation of Fund Transfer Service article. Also, remember all the cahanges made in this article are to be made in the Fund Transfer Service Application.
All the modifications we are implementing are for the F 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
In this layer, we shall forge DTO classes to facilitate inter-service communication. We shall craft the Account
and Transaction
classes within the model.dto
package. These entities serve as instrumental conduits for acquiring data in the course of inter-service communication.
Account.java
package org.training.fundtransfer.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Account {
private Long accountId;
private String accountNumber;
private String accountType;
private String accountStatus;
private BigDecimal availableBalance;
private Long userId;
}
Transaction.java
package org.training.fundtransfer.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Transaction {
private String accountId;
private String transactionType;
private BigDecimal amount;
private String description;
}
External Layer
We shall create an additional layer, encompassing the interfaces through which we facilitate the methods for inter-service communication. Within the external
package, we shall conceive the AccountService
and TransactionService
.
AccountService.java
package org.training.fundtransfer.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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.training.fundtransfer.configuration.FeignClientConfiguration;
import org.training.fundtransfer.model.dto.Account;
import org.training.fundtransfer.model.dto.response.Response;
@FeignClient(name = "account-service", configuration = FeignClientConfiguration.class)
public interface AccountService {
@GetMapping("/accounts")
ResponseEntity<Account> readByAccountNumber(@RequestParam String accountNumber);
@PutMapping("/accounts")
ResponseEntity<Response> updateAccount(@RequestParam String accountNumber, @RequestBody Account account);
}
TransactionService.java
package org.training.fundtransfer.external;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.training.fundtransfer.configuration.FeignClientConfiguration;
import org.training.fundtransfer.model.dto.Transaction;
import org.training.fundtransfer.model.dto.response.Response;
import java.util.List;
@FeignClient(name = "transaction-service", configuration = FeignClientConfiguration.class)
public interface TransactionService {
@PostMapping("/transactions")
ResponseEntity<Response> makeTransaction(@RequestBody Transaction transaction);
@PostMapping("/transactions/internal")
ResponseEntity<Response> makeInternalTransactions(@RequestBody List<Transaction> transactions,@RequestParam String transactionReference);
}
Service Layer
We shall utilize the methods for account verification and retrieval of necessary account details essential for initiating fund transfers. Moreover, we shall employ the transaction methods to incorporate the relevant transaction details into the transaction records.
In the below code we have just removed the comments, that we had added before in the article Mastering Microservices: Implemenatation of Fund Transfer Service.
Now, in the following code, I have delineated the contexts where we intend to apply the methods that are currently under implementation through the Feign Client.
@Override
public FundTransferResponse fundTransfer(FundTransferRequest fundTransferRequest) {
Account fromAccount;
ResponseEntity<Account> response = accountService.readByAccountNumber(fundTransferRequest.getFromAccount());
if(Objects.isNull(response.getBody())){
log.error("requested account "+fundTransferRequest.getFromAccount()+" is not found on the server");
throw new ResourceNotFound("requested account not found on the server", GlobalErrorCode.NOT_FOUND);
}
fromAccount = response.getBody();
if (!fromAccount.getAccountStatus().equals("ACTIVE")) {
log.error("account status is pending or inactive, please update the account status");
throw new AccountUpdateException("account is status is :pending", GlobalErrorCode.NOT_ACCEPTABLE);
}
if (fromAccount.getAvailableBalance().compareTo(fundTransferRequest.getAmount()) < 0) {
log.error("required amount to transfer is not available");
throw new InsufficientBalance("requested amount is not available", GlobalErrorCode.NOT_ACCEPTABLE);
}
Account toAccount;
response = accountService.readByAccountNumber(fundTransferRequest.getToAccount());
if(Objects.isNull(response.getBody())) {
log.error("requested account "+fundTransferRequest.getToAccount()+" is not found on the server");
throw new ResourceNotFound("requested account not found on the server", GlobalErrorCode.NOT_FOUND);
}
toAccount = response.getBody();
String transactionId = internalTransfer(fromAccount, toAccount, fundTransferRequest.getAmount());
FundTransfer fundTransfer = FundTransfer.builder()
.transferType(TransferType.INTERNAL)
.amount(fundTransferRequest.getAmount())
.fromAccount(fromAccount.getAccountNumber())
.transactionReference(transactionId)
.status(TransactionStatus.SUCCESS)
.toAccount(toAccount.getAccountNumber()).build();
fundTransferRepository.save(fundTransfer);
return FundTransferResponse.builder()
.transactionId(transactionId)
.message("Fund transfer was successful").build();
}
- In this context, we are employing the method
readByAccountNumber(@RequestParam String accountNumber)
to retrieve the necessary account information from the Account Service. Subsequently, we scrutinize the account attributes to facilitate the fund transfer process.
private String internalTransfer(Account fromAccount, Account toAccount, BigDecimal amount) {
fromAccount.setAvailableBalance(fromAccount.getAvailableBalance().subtract(amount));
accountService.updateAccount(fromAccount.getAccountNumber(), fromAccount);
toAccount.setAvailableBalance(toAccount.getAvailableBalance().add(amount));
accountService.updateAccount(toAccount.getAccountNumber(), toAccount);
List<Transaction> transactions = List.of(
Transaction.builder()
.accountId(fromAccount.getAccountNumber())
.transactionType("INTERNAL_TRANSFER")
.amount(amount.negate())
.description("Internal fund transfer from "+fromAccount.getAccountNumber()+" to "+toAccount.getAccountNumber())
.build(),
Transaction.builder()
.accountId(toAccount.getAccountNumber())
.transactionType("INTERNAL_TRANSFER")
.amount(amount)
.description("Internal fund transfer received from: "+fromAccount.getAccountNumber()).build());
String transactionReference = UUID.randomUUID().toString();
transactionService.makeInternalTransactions(transactions, transactionReference);
return transactionReference;
}
In the aforementioned code, we utilize the
updateAccount(@RequestParam String accountNumber, @RequestBody Account account)
method to modify the Account entity. This adjustment occurs when the specified amount is either credited or debited from the account.Furthermore, we intend to employ the
makeInternalTransactions(@RequestBody List<Transaction> transactions, @RequestParam String transactionReference)
method to incorporate the transactions executed during the fund transfer between the two accounts.
The complete code is given below:
FundTransferServiceImpl.java
package org.training.fundtransfer.service.implementation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.training.fundtransfer.exception.AccountUpdateException;
import org.training.fundtransfer.exception.GlobalErrorCode;
import org.training.fundtransfer.exception.InsufficientBalance;
import org.training.fundtransfer.exception.ResourceNotFound;
import org.training.fundtransfer.external.AccountService;
import org.training.fundtransfer.external.TransactionService;
import org.training.fundtransfer.model.mapper.FundTransferMapper;
import org.training.fundtransfer.model.TransactionStatus;
import org.training.fundtransfer.model.TransferType;
import org.training.fundtransfer.model.dto.Account;
import org.training.fundtransfer.model.dto.FundTransferDto;
import org.training.fundtransfer.model.dto.Transaction;
import org.training.fundtransfer.model.dto.request.FundTransferRequest;
import org.training.fundtransfer.model.dto.response.FundTransferResponse;
import org.training.fundtransfer.model.entity.FundTransfer;
import org.training.fundtransfer.repository.FundTransferRepository;
import org.training.fundtransfer.service.FundTransferService;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class FundTransferServiceImpl implements FundTransferService {
private final AccountService accountService;
private final FundTransferRepository fundTransferRepository;
private final TransactionService transactionService;
@Value("${spring.application.ok}")
private String ok;
private final FundTransferMapper fundTransferMapper = new FundTransferMapper();
@Override
public FundTransferResponse fundTransfer(FundTransferRequest fundTransferRequest) {
Account fromAccount;
ResponseEntity<Account> response = accountService.readByAccountNumber(fundTransferRequest.getFromAccount());
if(Objects.isNull(response.getBody())){
log.error("requested account "+fundTransferRequest.getFromAccount()+" is not found on the server");
throw new ResourceNotFound("requested account not found on the server", GlobalErrorCode.NOT_FOUND);
}
fromAccount = response.getBody();
if (!fromAccount.getAccountStatus().equals("ACTIVE")) {
log.error("account status is pending or inactive, please update the account status");
throw new AccountUpdateException("account is status is :pending", GlobalErrorCode.NOT_ACCEPTABLE);
}
if (fromAccount.getAvailableBalance().compareTo(fundTransferRequest.getAmount()) < 0) {
log.error("required amount to transfer is not available");
throw new InsufficientBalance("requested amount is not available", GlobalErrorCode.NOT_ACCEPTABLE);
}
Account toAccount;
response = accountService.readByAccountNumber(fundTransferRequest.getToAccount());
if(Objects.isNull(response.getBody())) {
log.error("requested account "+fundTransferRequest.getToAccount()+" is not found on the server");
throw new ResourceNotFound("requested account not found on the server", GlobalErrorCode.NOT_FOUND);
}
toAccount = response.getBody();
String transactionId = internalTransfer(fromAccount, toAccount, fundTransferRequest.getAmount());
FundTransfer fundTransfer = FundTransfer.builder()
.transferType(TransferType.INTERNAL)
.amount(fundTransferRequest.getAmount())
.fromAccount(fromAccount.getAccountNumber())
.transactionReference(transactionId)
.status(TransactionStatus.SUCCESS)
.toAccount(toAccount.getAccountNumber()).build();
fundTransferRepository.save(fundTransfer);
return FundTransferResponse.builder()
.transactionId(transactionId)
.message("Fund transfer was successful").build();
}
private String internalTransfer(Account fromAccount, Account toAccount, BigDecimal amount) {
fromAccount.setAvailableBalance(fromAccount.getAvailableBalance().subtract(amount));
accountService.updateAccount(fromAccount.getAccountNumber(), fromAccount);
toAccount.setAvailableBalance(toAccount.getAvailableBalance().add(amount));
accountService.updateAccount(toAccount.getAccountNumber(), toAccount);
List<Transaction> transactions = List.of(
Transaction.builder()
.accountId(fromAccount.getAccountNumber())
.transactionType("INTERNAL_TRANSFER")
.amount(amount.negate())
.description("Internal fund transfer from "+fromAccount.getAccountNumber()+" to "+toAccount.getAccountNumber())
.build(),
Transaction.builder()
.accountId(toAccount.getAccountNumber())
.transactionType("INTERNAL_TRANSFER")
.amount(amount)
.description("Internal fund transfer received from: "+fromAccount.getAccountNumber()).build());
String transactionReference = UUID.randomUUID().toString();
transactionService.makeInternalTransactions(transactions, transactionReference);
return transactionReference;
}
@Override
public FundTransferDto getTransferDetailsFromReferenceId(String referenceId) {
return fundTransferRepository.findFundTransferByTransactionReference(referenceId)
.map(fundTransferMapper::convertToDto)
.orElseThrow(() -> new ResourceNotFound("Fund transfer not found", GlobalErrorCode.NOT_FOUND));
}
@Override
public List<FundTransferDto> getAllTransfersByAccountId(String accountId) {
return fundTransferMapper.convertToDtoList(fundTransferRepository.findFundTransferByFromAccount(accountId));
}
}
Controller Layer
Since we added all the methods in the Fund Transfer Service in the article Mastering Microservices: Implemenatation of Fund Transfer Service, we don't need to add any other methods.
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- 3 with practical usage.
You can get source code for this tutorial from our GitHub repository.
Happy Coding!!!!π
Subscribe to my newsletter
Read articles from Karthik Kulkarni directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Karthik Kulkarni
Karthik Kulkarni
CSE'23 Grad π | Aspiring Java Developer π | Proficient in Spring, Spring Boot, REST APIs, Postman π» | Ready to Contribute and Grow π