๐ Welcome to our Blog on Implementing the Transaction Service in a Spring Boot Microservice Banking Application! ๐๐
Embark on a journey to master the intricacies of transaction management 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 Transaction Service.
Let's outline the functionalities we'll be building:
๐ Creating a Transaction Endpoint: Enable users to initiate various transactions, whether it's depositing funds, withdrawing money, or performing other financial activities.
๐ Retrieving All Transactions for an Account Endpoint: Provide account holders with the ability to retrieve a comprehensive list of all transactions associated with their specific account.
๐ Retrieving Details of a Specific Transaction Endpoint: Allow users to drill down into the specifics of a particular transaction, gaining insights into the details and context of individual financial activities.
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 will create a Transaction
class in the model.entity
package. This class comprises essential attributes including referenceId, accountId, transactionType, amount, transactionDate, transactionStatus, and comments. These attributes contribute to enhancing the functionality of the transaction process.
Transaction.java
package org.training.transactions.model.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.training.transactions.model.TransactionStatus;
import org.training.transactions.model.TransactionType;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long transactionId;
private String referenceId;
private String accountId;
@Enumerated(EnumType.STRING)
private TransactionType transactionType;
private BigDecimal amount;
@CreationTimestamp
private LocalDateTime transactionDate;
@Enumerated(EnumType.STRING)
private TransactionStatus status;
private String comments;
}
Enums
We will also create two enums, TransactionType
and TransactionStatus
, in the model
package, serving as concise representations of account status (e.g., PENDING
, COMPLETED
)and types (e.g., DEPOSIT
, WITHDRAWAL
, INTERNAL_TRANSFER
, EXTERNAL_TRANSFER
). Enums enhance code clarity, ease of maintenance, and reduce the risk of errors related to status and type values in the application.
TransactionType.java
package org.training.transactions.model;
public enum TransactionType {
DEPOSIT, WITHDRAWAL, INTERNAL_TRANSFER, EXTERNAL_TRANSFER
}
TransactionStatus.java
package org.training.transactions.model;
public enum TransactionStatus {
COMPLETED, PENDING
}
DTOs, Mappers, REST responses and requests
DTOs streamline data exchange, preventing over-fetching or under-fetching. Mappers enable seamless translation between layers, promoting modularity. RESTful responses and requests ensure standardized client-server interactions, supporting statelessness. This synergy optimizes data flow, enhances security, and aligns with RESTful principles for efficient communication in applications.
Data Transfer Objects(DTOs)
TransactionDto.java
package org.training.transactions.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.training.transactions.model.TransactionType;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TransactionDto {
private String accountId;
private String transactionType;
private BigDecimal amount;
private String description;
}
Mappers
BaseMapper.java
package org.training.transactions.model.mapper;
import java.util.Collection;
import java.util.List;
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> entity, Object... args) {
return entity.stream().map(e -> convertToDto(e, args)).collect(Collectors.toList());
}
public List<E> convertToEntityList(Collection<D> dto, Object... args) {
return convertToEntity(dto, args).stream().toList();
}
public List<D> convertToDtoList(Collection<E> entity, Object... args) {
return convertToDto(entity, args).stream().toList();
}
}
TransactionMapper.java
package org.training.transactions.model.mapper;
import org.springframework.beans.BeanUtils;
import org.training.transactions.model.dto.TransactionDto;
import org.training.transactions.model.entity.Transaction;
import java.util.Objects;
public class TransactionMapper extends BaseMapper<Transaction, TransactionDto> {
@Override
public Transaction convertToEntity(TransactionDto dto, Object... args) {
Transaction transaction = new Transaction();
if(!Objects.isNull(dto)){
BeanUtils.copyProperties(dto, transaction);
}
return transaction;
}
@Override
public TransactionDto convertToDto(Transaction entity, Object... args) {
TransactionDto transactionDto = new TransactionDto();
if(!Objects.isNull(entity)) {
BeanUtils.copyProperties(entity, transactionDto);
}
return transactionDto; }
}
REST responses and requests
TransactionRequest.java
package org.training.transactions.model.response;
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 TransactionRequest {
private String referenceId;
private String accountId;
private String transactionType;
private BigDecimal amount;
private LocalDateTime localDateTime;
private String transactionStatus;
private String comments;
}
Response.java
package org.training.transactions.model.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 is primarily an interface that contains high-level methods facilitating interaction with the database. Here, we are creating the TransactionRepository
interface in the repository
package.
TransactionRepository.java
package org.training.transactions.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.training.transactions.model.entity.Transaction;
import java.util.List;
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
List<Transaction> findTransactionByAccountId(String accountId);
List<Transaction> findTransactionByReferenceId(String referenceId);
}
Service Layer
We will create various methods for account operations such as making transaction and reterival. Additionally, we will design extra methods to facilitate communication between services for extracting additional account-related information, a feature we plan to implement in the near future.
TransactionService.java
package org.training.transactions.service;
import org.training.transactions.model.dto.TransactionDto;
import org.training.transactions.model.response.Response;
import org.training.transactions.model.response.TransactionRequest;
import java.util.List;
public interface TransactionService {
Response addTransaction(TransactionDto transactionDto);
Response internalTransaction(List<TransactionDto> transactionDtos, String transactionReference);
List<TransactionRequest> getTransaction(String accountId);
List<TransactionRequest> getTransactionByTransactionReference(String transactionReference);
}
package org.training.transactions.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.transactions.exception.AccountStatusException;
import org.training.transactions.exception.GlobalErrorCode;
import org.training.transactions.exception.InsufficientBalance;
import org.training.transactions.exception.ResourceNotFound;
import org.training.transactions.external.AccountService;
import org.training.transactions.model.TransactionStatus;
import org.training.transactions.model.TransactionType;
import org.training.transactions.model.dto.TransactionDto;
import org.training.transactions.model.entity.Transaction;
import org.training.transactions.model.external.Account;
import org.training.transactions.model.mapper.TransactionMapper;
import org.training.transactions.model.response.Response;
import org.training.transactions.model.response.TransactionRequest;
import org.training.transactions.repository.TransactionRepository;
import org.training.transactions.service.TransactionService;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class TransactionServiceImpl implements TransactionService {
private final TransactionRepository transactionRepository;
//private final AccountService accountService;
private final TransactionMapper transactionMapper = new TransactionMapper();
@Value("${spring.application.ok}")
private String ok;
@Override
public Response addTransaction(TransactionDto transactionDto) {
/*
ResponseEntity<Account> response = accountService.readByAccountNumber(transactionDto.getAccountId());
if (Objects.isNull(response.getBody())){
throw new ResourceNotFound("Requested account not found on the server", GlobalErrorCode.NOT_FOUND);
} */
Account account = response.getBody();
Transaction transaction = transactionMapper.convertToEntity(transactionDto);
if(transactionDto.getTransactionType().equals(TransactionType.DEPOSIT.toString())) {
account.setAvailableBalance(account.getAvailableBalance().add(transactionDto.getAmount()));
} else if (transactionDto.getTransactionType().equals(TransactionType.WITHDRAWAL.toString())) {
if(!account.getAccountStatus().equals("ACTIVE")){
log.error("account is either inactive/closed, cannot process the transaction");
throw new AccountStatusException("account is inactive or closed");
}
if(account.getAvailableBalance().compareTo(transactionDto.getAmount()) < 0){
log.error("insufficient balance in the account");
throw new InsufficientBalance("Insufficient balance in the account");
}
transaction.setAmount(transactionDto.getAmount().negate());
account.setAvailableBalance(account.getAvailableBalance().subtract(transactionDto.getAmount()));
}
transaction.setTransactionType(TransactionType.valueOf(transactionDto.getTransactionType()));
transaction.setComments(transactionDto.getDescription());
transaction.setStatus(TransactionStatus.COMPLETED);
transaction.setReferenceId(UUID.randomUUID().toString());
//accountService.updateAccount(transactionDto.getAccountId(), account);
transactionRepository.save(transaction);
return Response.builder()
.message("Transaction completed successfully")
.responseCode(ok).build();
}
@Override
public Response internalTransaction(List<TransactionDto> transactionDtos, String transactionReference) {
List<Transaction> transactions = transactionMapper.convertToEntityList(transactionDtos);
transactions.forEach(transaction -> {
transaction.setTransactionType(TransactionType.INTERNAL_TRANSFER);
transaction.setStatus(TransactionStatus.COMPLETED);
transaction.setReferenceId(transactionReference);
});
transactionRepository.saveAll(transactions);
return Response.builder()
.responseCode(ok)
.message("Transaction completed successfully").build();
}
@Override
public List<TransactionRequest> getTransaction(String accountId) {
return transactionRepository.findTransactionByAccountId(accountId)
.stream().map(transaction -> {
TransactionRequest transactionRequest = new TransactionRequest();
BeanUtils.copyProperties(transaction, transactionRequest);
transactionRequest.setTransactionStatus(transaction.getStatus().toString());
transactionRequest.setLocalDateTime(transaction.getTransactionDate());
transactionRequest.setTransactionType(transaction.getTransactionType().toString());
return transactionRequest;
}).collect(Collectors.toList());
}
@Override
public List<TransactionRequest> getTransactionByTransactionReference(String transactionReference) {
return transactionRepository.findTransactionByReferenceId(transactionReference)
.stream().map(transaction -> {
TransactionRequest transactionRequest = new TransactionRequest();
BeanUtils.copyProperties(transaction, transactionRequest);
transactionRequest.setTransactionStatus(transaction.getStatus().toString());
transactionRequest.setLocalDateTime(transaction.getTransactionDate());
transactionRequest.setTransactionType(transaction.getTransactionType().toString());
return transactionRequest;
}).collect(Collectors.toList());
}
}
Controller Layer
Now, let's expose all the methods as API endpoints.
package org.training.transactions.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.transactions.model.dto.TransactionDto;
import org.training.transactions.model.response.Response;
import org.training.transactions.model.response.TransactionRequest;
import org.training.transactions.service.TransactionService;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/transactions")
public class TransactionController {
private final TransactionService transactionService;
@PostMapping
public ResponseEntity<Response> addTransactions(@RequestBody TransactionDto transactionDto) {
return new ResponseEntity<>(transactionService.addTransaction(transactionDto), HttpStatus.CREATED);
}
@PostMapping("/internal")
public ResponseEntity<Response> makeInternalTransaction(@RequestBody List<TransactionDto> transactionDtos,@RequestParam String transactionReference) {
return new ResponseEntity<>(transactionService.internalTransaction(transactionDtos, transactionReference), HttpStatus.CREATED);
}
@GetMapping
public ResponseEntity<List<TransactionRequest>> getTransactions(@RequestParam String accountId) {
return new ResponseEntity<>(transactionService.getTransaction(accountId), HttpStatus.OK);
}
@GetMapping("/{referenceId}")
public ResponseEntity<List<TransactionRequest>> getTransactionByTransactionReference(@PathVariable String referenceId) {
return new ResponseEntity<>(transactionService.getTransactionByTransactionReference(referenceId), HttpStatus.OK);
}
}
Adding all the configuration essential in application.yml
:
spring:
application:
name: transaction-service
bad_request: 400
conflict: 409
ok: 200
datasource:
url: jdbc:mysql://localhost:3306/transaction_service
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8084
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: transaction-service
uri: lb://transaction-service
predicates:
- Path=/transactions/**
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 Transaction Service with practical usage.
You can get source code for this tutorial from our GitHub repository.
Happy Coding!!!!๐