Mastering Microservices: Implemenatation of Account Service

๐Ÿš€ 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.

Run In Postman

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!!!!๐Ÿ˜Š

1
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 ๐Ÿš€