I want to create a very simple banking application where (i) predefined accounts with a positive balance can send money (ii) requesting balance and a list of transactions can be found.
I have created API for those operations. All the operations are persisted into the database. I also wrote test cases to verify.
I always try to learn from different sources and use the best practices related to project structure and clean code. It would be great if somebody gives me suggestions and ideas about what improvement I can make on the codes.
Here is my project structure:
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── forhadmethun
│ │ │ └── banking
│ │ │ ├── BankingApplication.java
│ │ │ ├── config
│ │ │ │ └── documentation
│ │ │ │ └── DocumentationConfig.java
│ │ │ ├── controller
│ │ │ │ ├── api
│ │ │ │ │ └── AccountController.java
│ │ │ │ └── request
│ │ │ │ ├── AccountStatementRequest.java
│ │ │ │ └── TransferBalanceRequest.java
│ │ │ ├── dto
│ │ │ │ ├── model
│ │ │ │ │ └── AccountStatement.java
│ │ │ │ └── response
│ │ │ │ ├── ResponseError.java
│ │ │ │ └── Response.java
│ │ │ ├── entity
│ │ │ │ ├── Account.java
│ │ │ │ └── Transaction.java
│ │ │ ├── repository
│ │ │ │ ├── AccountRepository.java
│ │ │ │ └── TransactionRepository.java
│ │ │ └── service
│ │ │ ├── AccountService.java
│ │ │ └── impl
│ │ │ └── AccountServiceImpl.java
│ │ └── resources
│ │ ├── application.properties
│ │ ├── static
│ │ └── templates
│ └── test
│ └── java
│ └── com
│ └── forhadmethun
│ └── banking
│ └── service
│ └── impl
│ └── AccountServiceImplTest.java
AccountController.java
@RestController
@RequestMapping("/api/account")
public class AccountController {
@Autowired private AccountService accountService;
@RequestMapping("/create")
public List<Account> create(@RequestBody Account account) {
accountService.save(account);
return accountService.findAll();
}
@RequestMapping("/all")
public List<Account> all() {
return accountService.findAll();
}
@RequestMapping("/sendmoney")
public Response sendMoney(
@RequestBody TransferBalanceRequest transferBalanceRequest
) {
return Response.ok().setPayload(
accountService.sendMoney(
transferBalanceRequest
)
);
}
@RequestMapping("/statement")
public Response getStatement(
@RequestBody AccountStatementRequest accountStatementRequest
){
return Response.ok().setPayload(
accountService.getStatement(accountStatementRequest.getAccountNumber())
);
}
}
AccountStatementRequest.java
@Getter
@Setter
@NoArgsConstructor
public class AccountStatementRequest {
private String accountNumber;
}
TransferBalanceRequest.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TransferBalanceRequest {
private String fromAccountNumber;
private String toAccountNumber;
private BigDecimal amount;
}
AccountStatement.java
@AllArgsConstructor
@NoArgsConstructor
@Data
public class AccountStatement {
BigDecimal currentBalance;
List<Transaction> transactionHistory;
}
Response.java
package com.forhadmethun.banking.dto.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Date;
@Getter
@Setter
@Accessors(chain = true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Response<T> {
private Status status;
private T payload;
private Object errors;
private Object metadata;
public static <T> Response<T> badRequest() {
Response<T> response = new Response<>();
response.setStatus(Status.BAD_REQUEST);
return response;
}
public static <T> Response<T> ok() {
Response<T> response = new Response<>();
response.setStatus(Status.OK);
return response;
}
public static <T> Response<T> unauthorized() {
Response<T> response = new Response<>();
response.setStatus(Status.UNAUTHORIZED);
return response;
}
public static <T> Response<T> validationException() {
Response<T> response = new Response<>();
response.setStatus(Status.VALIDATION_EXCEPTION);
return response;
}
public static <T> Response<T> wrongCredentials() {
Response<T> response = new Response<>();
response.setStatus(Status.WRONG_CREDENTIALS);
return response;
}
public static <T> Response<T> accessDenied() {
Response<T> response = new Response<>();
response.setStatus(Status.ACCESS_DENIED);
return response;
}
public static <T> Response<T> exception() {
Response<T> response = new Response<>();
response.setStatus(Status.EXCEPTION);
return response;
}
public static <T> Response<T> notFound() {
Response<T> response = new Response<>();
response.setStatus(Status.NOT_FOUND);
return response;
}
public static <T> Response<T> duplicateEntity() {
Response<T> response = new Response<>();
response.setStatus(Status.DUPLICATE_ENTITY);
return response;
}
public void addErrorMsgToResponse(String errorMsg, Exception ex) {
ResponseError error = new ResponseError()
.setDetails(errorMsg)
.setMessage(ex.getMessage())
.setTimestamp(new Date());
setErrors(error);
}
public enum Status {
OK, BAD_REQUEST, UNAUTHORIZED, VALIDATION_EXCEPTION, EXCEPTION, WRONG_CREDENTIALS, ACCESS_DENIED, NOT_FOUND, DUPLICATE_ENTITY
}
@Getter
@Accessors(chain = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PageMetadata {
private int size;
private long totalElements;
private int totalPages;
private int number;
public PageMetadata(int size, long totalElements, int totalPages, int number) {
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.number = number;
}
}
}
ResponseError.java
@Getter
@Setter
@Accessors(chain = true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResponseError {
private Date timestamp;
private String message;
private String details;
}
Account.java
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "account")
public class Account {
@Id
@GeneratedValue
private Long accountId;
String accountNumber;
BigDecimal currentBalance;
}
Transaction.java
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "transaction")
public class Transaction {
@Id
@GeneratedValue
private Long transactionId;
private String accountNumber;
private BigDecimal transactionAmount;
private Timestamp transactionDateTime;
}
AccountRepository.java
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
Account findByAccountNumberEquals(String accountNumber);
}
TransactionRepository.java
@Repository
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
List<Transaction> findByAccountNumberEquals(String accountNumber);
}
AccountService.java
public interface AccountService {
List<Account> findAll();
Account save(Account account);
Transaction sendMoney(
TransferBalanceRequest transferBalanceRequest
);
AccountStatement getStatement(String accountNumber);
}
AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountRepository accountRepository;
@Autowired
TransactionRepository transactionRepository;
public Account save(Account account){
accountRepository.save(account);
return accountRepository.findByAccountNumberEquals(account.getAccountNumber());
}
public List<Account> findAll(){
return accountRepository.findAll();
}
public Account findByAccountNumber(String accountNumber){
Account account = accountRepository.findByAccountNumberEquals(accountNumber);
return account;
}
@Override
public Transaction sendMoney(
TransferBalanceRequest transferBalanceRequest
) {
String fromAccountNumber = transferBalanceRequest.getFromAccountNumber();
String toAccountNumber = transferBalanceRequest.getToAccountNumber();
BigDecimal amount = transferBalanceRequest.getAmount();
Account fromAccount = accountRepository.findByAccountNumberEquals(
fromAccountNumber
);
Account toAccount = accountRepository.findByAccountNumberEquals(toAccountNumber);
if(fromAccount.getCurrentBalance().compareTo(BigDecimal.ONE) == 1
&& fromAccount.getCurrentBalance().compareTo(amount) == 1
){
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance().subtract(amount));
accountRepository.save(fromAccount);
toAccount.setCurrentBalance(toAccount.getCurrentBalance().add(amount));
accountRepository.save(toAccount);
Transaction transaction = transactionRepository.save(new Transaction(0L,fromAccountNumber,amount,new Timestamp(System.currentTimeMillis())));
return transaction;
}
return null;
}
@Override
public AccountStatement getStatement(String accountNumber) {
Account account = accountRepository.findByAccountNumberEquals(accountNumber);
return new AccountStatement(account.getCurrentBalance(),transactionRepository.findByAccountNumberEquals(accountNumber));
}
AccountServiceImplTest.java
@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountServiceImplTest {
@TestConfiguration
static class AccountServiceTestContextConfiguration {
@Bean
public AccountServiceImpl accountServiceImplTest() {
return new AccountServiceImpl();
}
}
@Autowired
private AccountServiceImpl accountService;
@Test
public void sendMoneyTest() {
Account account1 = new Account(0L, "1001", new BigDecimal(50000));
Account account2 = new Account(0L, "2002", new BigDecimal(2000));
accountService.save(account1);
accountService.save(account2);
TransferBalanceRequest transferBalanceRequest =
new TransferBalanceRequest(
account1.getAccountNumber(),
account2.getAccountNumber(),
new BigDecimal(3000)
);
accountService.sendMoney(transferBalanceRequest);
assertThat(accountService.findByAccountNumber(account1.getAccountNumber())
.getCurrentBalance())
.isEqualTo(new BigDecimal(47000));
assertThat(accountService.findByAccountNumber(account2.getAccountNumber())
.getCurrentBalance())
.isEqualTo(new BigDecimal(5000));
}
@Test
public void getStatement() {
Account account1 = new Account(0L, "1001", new BigDecimal(50000));
Account account2 = new Account(0L, "2002", new BigDecimal(2000));
accountService.save(account1);
accountService.save(account2);
TransferBalanceRequest transferBalanceRequest =
new TransferBalanceRequest(
account1.getAccountNumber(),
account2.getAccountNumber(),
new BigDecimal(3000)
);
accountService.sendMoney(transferBalanceRequest);
assertThat(accountService.getStatement(account1.getAccountNumber())
.getCurrentBalance())
.isEqualTo(new BigDecimal(47000));
accountService.sendMoney(transferBalanceRequest);
assertThat(accountService.getStatement(account1.getAccountNumber())
.getCurrentBalance()).isEqualTo(new BigDecimal(44000));
assertThat(accountService.getStatement(account1.getAccountNumber())
.getTransactionHistory().size()).isEqualTo(2);
}
}
```