๐ Welcome to our Blog on Implementing the Account Service Management in a Spring Boot Microservice Banking Application! ๐๐ผ
Explore the fundamental aspects and key insights influencing account management within our microservices-based banking system. Whether you're a developer or a tech enthusiast, join us for hands-on guidance and an in-depth exploration of constructing a resilient Account Service.
So what are the functionalities that we are gonna build?
๐Account Creation Endpoint: Users have the ability to create new bank accounts.
๐Account Update Endpoint: Account holders can modify certain details of their accounts, such as account type or owner information.
๐ชAccount Closure Endpoint: Users are empowered to close their accounts through a dedicated endpoint.
๐Read Account by Account Number Endpoint: Users can retrieve details of an account based on its account number, focusing on active accounts.
๐Read Transactions for Account Endpoint: Account holders can access a list of transactions associated with their specific account.
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 are going to create an Account class in the model.entity
package, defining a concise representation of a bank account in a Spring Boot application. The class includes attributes such as accountId, accountNumber, accountType, and availableBalance, leveraging annotations like @Entity
and @Data
for streamlined code, and JPA annotations for database configurations.
Account.java
package org.training.account.service.model.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.training.account.service.model.AccountStatus;
import org.training.account.service.model.AccountType;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long accountId;
private String accountNumber;
@Enumerated(EnumType.STRING)
private AccountType accountType;
@Enumerated(EnumType.STRING)
private AccountStatus accountStatus;
@CreationTimestamp
private LocalDate openingDate;
private BigDecimal availableBalance;
private Long userId;
}
Enums
We will also create two enums, AccountStatus
and AccountType
, in the model
package, serving as concise representations of account status (e.g., PENDING
, ACTIVE
, BLOCKED
, CLOSED
)and types (e.g., SAVINGS_ACCOUNT
, FIXED_DEPOSIT
, LOAN_ACCOUNT
). Enums enhance code clarity, ease of maintenance, and reduce the risk of errors related to status and type values in the application.
AccountStatus.java
package org.training.account.service.model;
public enum AccountStatus {
PENDING, ACTIVE, BLOCKED, CLOSED
}
AccountType.java
package org.training.account.service.model;
public enum AccountType {
SAVINGS_ACCOUNT, FIXED_DEPOSIT, LOAN_ACCOUNT
}
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.
AccountDto.java
package org.training.account.service.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.training.account.service.model.AccountStatus;
import org.training.account.service.model.AccountType;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AccountDto {
private Long accountId;
private String accountNumber;
private String accountType;
private String accountStatus;
private BigDecimal availableBalance;
private Long userId;
}
AccountStatusUpdate.java
package org.training.account.service.model.dto;
import lombok.Data;
import org.training.account.service.model.AccountStatus;
@Data
public class AccountStatusUpdate {
AccountStatus accountStatus;
}
BaseMapper.java
package org.training.account.service.model.mapper;
import javax.swing.event.ListDataEvent;
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> entities, Object... args) {
return entities.stream().map(entity -> convertToDto(entity, 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> entities, Object... args) {
return convertToDto(entities, args).stream().toList();
}
}
AccountMapper.java
package org.training.account.service.model.mapper;
import org.springframework.beans.BeanUtils;
import org.training.account.service.model.dto.AccountDto;
import org.training.account.service.model.entity.Account;
import java.util.Objects;
public class AccountMapper extends BaseMapper<Account, AccountDto> {
@Override
public Account convertToEntity(AccountDto dto, Object... args) {
Account account = new Account();
if(!Objects.isNull(dto)){
BeanUtils.copyProperties(dto, account);
}
return account;
}
@Override
public AccountDto convertToDto(Account entity, Object... args) {
AccountDto accountDto = new AccountDto();
if(!Objects.isNull(entity)) {
BeanUtils.copyProperties(entity, accountDto);
}
return accountDto;
}
}
Repository Layer
The repository is primarily an interface that contains high-level methods facilitating interaction with the database. Here, we are creating the AccountRepository
interface in the repository
package.
package org.training.account.service.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.training.account.service.model.AccountType;
import org.training.account.service.model.entity.Account;
import java.util.Optional;
public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findAccountByUserIdAndAccountType(Long userId, AccountType accountType);
Optional<Account> findAccountByAccountNumber(String accountNumber);
Optional<Account> findAccountByUserId(Long userId);
}
Service Layer
We will create various methods for account operations such as creation, updating, status modification, retrieval, and closure. 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.
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.response.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);
}
In this interface, the method
getTransactionsFromAccountId(String accountId)
will be implemented later. This method require communication with other services.
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.response.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 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);
}
}
In the
createAccount(AccountDto accountDto)
method, I have commented out some lines that will be explained in future articles. These lines involve inter-service communication.
Controller Layer
Now, let's expose all the methods as API endpoints.
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.response.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));
}
@PutMapping("/closure")
public ResponseEntity<Response> closeAccount(@RequestParam String accountNumber) {
return ResponseEntity.ok(accountService.closeAccount(accountNumber));
}
}
Adding the configuration required in application.xml
file:
spring:
application:
name: account-service
bad_request: 400
conflict: 409
ok: 200
not_found: 404
datasource:
url: jdbc:mysql://localhost:3306/account_service
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8081
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: account-service
uri: lb://account-service
predicates:
- Path=/accounts/**
Note: Please don't try to run the application, since we need to add inter - service configuration and 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: Implemenatation of Account Service with practical usage.
You can get source code for this tutorial from our GitHub repository.
Happy Coding!!!!๐