๐ Welcome to Blog on Implementing the Fund Transfer Service in a Spring Boot Microservice Banking Application! ๐๐ธ
Embark on a journey to master the intricacies of fund transfers within our microservices-driven banking system. Whether you're a seasoned developer or a tech enthusiast, dive into practical guidance and an in-depth exploration of constructing a robust Fund Transfer Service. Let's delve into the world of seamless financial transactions! ๐
What are the functionalities we intend to construct?
๐ธIntiate funds transfer: Facilitate users in effortlessly transferring funds between accounts through a designated endpoint.
๐Get Details of all fund transfers: Retrieve comprehensive details of all fund transfers executed by the account, fostering transparency and facilitating auditing.
๐ต๏ธโโ๏ธGet details of fund transfer based on the reference ID: Retrieve comprehensive details regarding a specific fund transfer, empowering users and administrators with precise transactional information.
Development
Now, move to your favorite IDE or to the Spring Initializer and create a Spring boot Application with the following dependencies
Now, what are we gonna use?
Model Layer
Entity
Now, we'll craft the FundTransfer
Java class, in package model.entity
โa concise entity model for fund transfers. Annotated with @Entity
and leveraging Lombok for code brevity, this class encapsulates vital details including identifiers, account information, amount, status, type, and timestamp. The @CreationTimestamp
from Hibernate automates timestamp management. It also provides an efficient solution for persistently storing fund transfer data.
FundTransfer.java
package org.training.fundtransfer.model.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.training.fundtransfer.model.TransactionStatus;
import org.training.fundtransfer.model.TransferType;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class FundTransfer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long fundTransferId;
private String transactionReference;
private String fromAccount;
private String toAccount;
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private TransactionStatus status;
@Enumerated(EnumType.STRING)
private TransferType transferType;
@CreationTimestamp
private LocalDateTime transferredOn;
}
Enums
We will additionally craft two enums, TransactionStatus
and TransferType
, within the model
package. These enums serve as representations of transaction status (e.g., PENDING
, PROCESSING
, SUCCESS
, FAILED
) and types (e.g., WITHDRAWAL
, INTERNAL
, EXTERNAL
, CHEQUE
). Enums augment code clarity, simplify maintenance, and mitigate the risk of errors associated with status and type values in the application.
TransactionStatus.java
package org.training.fundtransfer.model;
public enum TransactionStatus {
PENDING, PROCESSING, SUCCESS, FAILED
}
TransferType.java
package org.training.fundtransfer.model;
public enum TransferType {
WITHDRAWAL, INTERNAL, EXTERNAL, CHEQUE
}
DTOs, Mappers, REST reaponses and requests
DTOs streamline data exchange, averting over-fetching or under-fetching. Mappers facilitate seamless translation between layers, fostering modularity. RESTful responses and requests guarantee standardized client-server interactions, bolstering statelessness. This synergy optimizes data flow, fortifies security, and aligns with RESTful principles for efficient communication in applications.
Data Transfer Objects(DTOs)
FundTransferDto.java
package org.training.fundtransfer.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.training.fundtransfer.model.TransactionStatus;
import org.training.fundtransfer.model.TransferType;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FundTransferDto {
private String transactionReference;
private String fromAccount;
private String toAccount;
private BigDecimal amount;
private TransactionStatus status;
private TransferType transferType;
private LocalDateTime transferredOn;
}
Mappers
BaseMapper.java
package org.training.fundtransfer.model.mapper;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public abstract class BaseMapper<E, D> {
public abstract E convertToEntity(D dto, Object... args);
public abstract D convertToDto(E entity, Object... args);
public Collection<E> convertToEntity(Collection<D> dto, Object... args) {
return dto.stream().map(d -> convertToEntity(d, args)).collect(Collectors.toList());
}
public Collection<D> convertToDto(Collection<E> entities, Object... args) {
return entities.stream().map(entity ->convertToDto(entity, args)).collect(Collectors.toList());
}
public List<E> covertToEntityList(Collection<D> dtos, Object... args) {
return convertToEntity(dtos, args).stream().collect(Collectors.toList());
}
public List<D> convertToDtoList(Collection<E> entities, Object... args) {
return convertToDto(entities, args).stream().collect(Collectors.toList());
}
public Set<E> convertToEntitySet(Collection<D> dtos, Object... args) {
return convertToEntity(dtos, args).stream().collect(Collectors.toSet());
}
}
FundTransferMapper.java
package org.training.fundtransfer.model.mapper;
import org.springframework.beans.BeanUtils;
import org.training.fundtransfer.model.dto.FundTransferDto;
import org.training.fundtransfer.model.entity.FundTransfer;
import java.util.Objects;
public class FundTransferMapper extends BaseMapper<FundTransfer, FundTransferDto> {
@Override
public FundTransfer convertToEntity(FundTransferDto dto, Object... args) {
FundTransfer fundTransfer = new FundTransfer();
if(!Objects.isNull(dto)){
BeanUtils.copyProperties(dto, fundTransfer);
}
return fundTransfer;
}
@Override
public FundTransferDto convertToDto(FundTransfer entity, Object... args) {
FundTransferDto fundTransferDto = new FundTransferDto();
if(!Objects.isNull(entity)){
BeanUtils.copyProperties(entity, fundTransferDto);
}
return fundTransferDto;
}
}
REST responses and requests
FundTransferRequest.java
package org.training.fundtransfer.model.dto.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FundTransferRequest {
private String fromAccount;
private String toAccount;
private BigDecimal amount;
}
FundTransferResponse.java
package org.training.fundtransfer.model.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FundTransferResponse {
private String transactionId;
private String message;
}
Response.java
package org.training.fundtransfer.model.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Response {
private String responseCode;
private String message;
}
Repository Layer
The repository serves as a fundamental interface housing high-level methods that ease interaction with the database. In this context, we are establishing the FundTransferRepository
interface within the repository
package.
FundTransferRepository.java
package org.training.fundtransfer.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.training.fundtransfer.model.entity.FundTransfer;
import java.util.List;
import java.util.Optional;
public interface FundTransferRepository extends JpaRepository<FundTransfer, Long> {
Optional<FundTransfer> findFundTransferByTransactionReference(String referenceId);
List<FundTransfer> findFundTransferByFromAccount(String accountId);
}
Service Layer
We will be formulating a service interface named FundTransferService
, encompassing various methods for executing fund transfers and retrieving transfer detailsโboth comprehensive records and individual transactions. Subsequently, the implementation of these methods will be implemented in the FundTransferServiceImpl
class in service
package.
FundTransferService.java
package org.training.fundtransfer.service;
import org.training.fundtransfer.model.dto.FundTransferDto;
import org.training.fundtransfer.model.dto.request.FundTransferRequest;
import org.training.fundtransfer.model.dto.response.FundTransferResponse;
import java.util.List;
public interface FundTransferService {
FundTransferResponse fundTransfer(FundTransferRequest fundTransferRequest);
FundTransferDto getTransferDetailsFromReferenceId(String referenceId);
List<FundTransferDto> getAllTransfersByAccountId(String accountId);
}
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));
}
}
In the above implementation, I have provided comments on the
private final AccountService accountService
andprivate final TransactionService transactionService
definitions. Additionally, I have commented onaccountService.readByAccountNumber()
,accountService.updateAccount()
, andtransactionService.makeInternalTransactions()
methods. These comments indicate that these methods involve interservice communication, a concept we will delve into in future articles.
Controller Layer
Now, let's expose all these methods as API endpoints.
FundTransferController.java
package org.training.fundtransfer.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.training.fundtransfer.model.dto.FundTransferDto;
import org.training.fundtransfer.model.dto.request.FundTransferRequest;
import org.training.fundtransfer.model.dto.response.FundTransferResponse;
import org.training.fundtransfer.service.FundTransferService;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/fund-transfers")
public class FundTransferController {
private final FundTransferService fundTransferService;
@PostMapping
public ResponseEntity<FundTransferResponse> fundTransfer(@RequestBody FundTransferRequest fundTransferRequest) {
return new ResponseEntity<>(fundTransferService.fundTransfer(fundTransferRequest), HttpStatus.CREATED);
}
@GetMapping("/{referenceId}")
public ResponseEntity<FundTransferDto> getTransferDetailsFromReferenceId(@PathVariable String referenceId) {
return new ResponseEntity<>(fundTransferService.getTransferDetailsFromReferenceId(referenceId), HttpStatus.OK);
}
@GetMapping
public ResponseEntity<List<FundTransferDto>> getAllTransfersByAccountId(@RequestParam String accountId) {
return new ResponseEntity<>(fundTransferService.getAllTransfersByAccountId(accountId), HttpStatus.OK);
}
}
All required configuration for the FundTransfer Service in added in application.yml
file:
server:
port: 8085
spring:
application:
name: fund-transfer-service
bad_request: 400
ok: 200
datasource:
url: jdbc:mysql://localhost:3306/fund_transfer_service
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
Now, add the routes defination required for the API Gateway that we created Mastering Microservices: Setting API Gateway with Spring Cloud Gateway to recogonize the service.
- id: fund-transfer-service
uri: lb://fund-transfer-service
predicates:
- Path=/api/fund-transfers/**
Note: Please don't try to run the application, since we need to add inter - service configuration and exception handling configuration.
Postman Collection
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: Implemenatation of Fund Transfer Service with practical usage.
You can get source code for this tutorial from our GitHub repository.
Happy Coding!!!!๐