diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ddff09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +### IntelliJ IDEA ### +.idea +target \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2ca15ec..0e222a3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,44 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - + + UTF-8 + 1.8 + 1.8 + ch.engenius accounts 1.0-SNAPSHOT - - + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + + org.projectlombok + lombok + 1.18.24 + provided + + + junit + junit + 4.13.2 + test + + + org.junit.jupiter + junit-jupiter + 5.9.0 + test + + \ No newline at end of file diff --git a/src/main/java/ch/engenius/bank/Account.java b/src/main/java/ch/engenius/bank/Account.java deleted file mode 100644 index b9979cb..0000000 --- a/src/main/java/ch/engenius/bank/Account.java +++ /dev/null @@ -1,31 +0,0 @@ -package ch.engenius.bank; - -import java.math.BigDecimal; - -public class Account { - private double money; - - public void withdraw(double amount) { - if ((money - amount) < 0) { - throw new IllegalStateException("not enough credits on account"); - } - setMoney(money - amount); - - } - - public void deposit(double amount) { - setMoney(money + amount); - } - - public double getMoney() { - return money; - } - - public void setMoney(double money) { - this.money = money; - } - - public BigDecimal getMoneyAsBigDecimal() { - return BigDecimal.valueOf(money); - } -} diff --git a/src/main/java/ch/engenius/bank/Bank.java b/src/main/java/ch/engenius/bank/Bank.java deleted file mode 100644 index 571ebc7..0000000 --- a/src/main/java/ch/engenius/bank/Bank.java +++ /dev/null @@ -1,18 +0,0 @@ -package ch.engenius.bank; - -import java.util.HashMap; - -public class Bank { - private HashMap accounts = new HashMap<>(); - - public Account registerAccount(int accountNumber, int amount) { - Account account = new Account(); - account.setMoney(amount); - accounts.put(accountNumber, account); - return account; - } - - public Account getAccount( int number) { - return accounts.get(number); - } -} diff --git a/src/main/java/ch/engenius/bank/BankRunner.java b/src/main/java/ch/engenius/bank/BankRunner.java index 10b30fd..500119c 100644 --- a/src/main/java/ch/engenius/bank/BankRunner.java +++ b/src/main/java/ch/engenius/bank/BankRunner.java @@ -1,7 +1,13 @@ package ch.engenius.bank; + +import ch.engenius.bank.domain.Bank; +import ch.engenius.bank.domain.BankAccount; +import ch.engenius.bank.exceptions.BankMoneyAmountException; + import java.math.BigDecimal; -import java.util.Random; +import java.util.HashMap; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -9,63 +15,68 @@ public class BankRunner { - private static final ExecutorService executor = Executors.newFixedThreadPool(8); - - private final Random random = new Random(43); - private final Bank bank = new Bank(); + private static final int NUMBER_OF_BANK_ACCOUNTS = 1000; + private static final int NUMBER_OF_ITERATIONS = 1000; + private static final BigDecimal DEFAULT_DEPOSIT = BigDecimal.valueOf(100); + public static final BigDecimal TOTAL_BANK_MONEY_AMOUNT = DEFAULT_DEPOSIT.multiply(BigDecimal.valueOf(NUMBER_OF_BANK_ACCOUNTS)); + private static final ExecutorService executorService = Executors.newFixedThreadPool(8); + private final Bank bank; + public BankRunner() { + HashMap accounts = new HashMap<>(); + this.bank = new Bank(accounts); + } public static void main(String[] args) { BankRunner runner = new BankRunner(); - int accounts = 100; - int defaultDeposit = 1000; - int iterations = 10000; - runner.registerAccounts(accounts, defaultDeposit); - runner.sanityCheck(accounts, accounts*defaultDeposit); - runner.runBank(iterations, accounts); - runner.sanityCheck(accounts, accounts*defaultDeposit); + runner.registerAccounts(); + runner.bankMoneyAmountCheck(); + runner.runTransactions(); + runner.bankMoneyAmountCheck(); + } + private void registerAccounts() { + IntStream.range(0, NUMBER_OF_BANK_ACCOUNTS).forEach(k -> + bank.registerBankAccount(UUID.randomUUID(), BankRunner.DEFAULT_DEPOSIT) + ); } - private void runBank(int iterations, int maxAccount) { - for (int i =0; i< iterations; i++ ) { - executor.submit( ()-> runRandomOperation(maxAccount)); + private void bankMoneyAmountCheck() { + BigDecimal currentTotalAmount = bank.getAccounts().values() + .stream() + .map(BankAccount::getMoneyAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + if (TOTAL_BANK_MONEY_AMOUNT.compareTo(currentTotalAmount) != 0) { + throw new BankMoneyAmountException("Bank money amount check failed."); } + System.out.println("Bank money amount check successful."); + } + + private void runTransactions() { + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + executorService.submit(this::transferMoneyFromAndToRandomBankAccount); + } + executorService.shutdown(); try { - executor.shutdown(); - executor.awaitTermination(100,TimeUnit.SECONDS); + if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } } catch (InterruptedException e) { - e.printStackTrace(); + System.out.println("InterruptedException occurred: " + e); + executorService.shutdownNow(); } } - private void runRandomOperation(int maxAccount) { - double transfer = random.nextDouble()*100.0; - int accountInNumber = random.nextInt(maxAccount); - int accountOutNumber = random.nextInt(maxAccount); - Account accIn =bank.getAccount(accountInNumber); - Account accOut =bank.getAccount(accountOutNumber); - accIn.deposit(transfer); - accOut.withdraw(transfer); - } + private void transferMoneyFromAndToRandomBankAccount() { + BigDecimal transferMoneyAmount = BigDecimal.valueOf(Math.random() * DEFAULT_DEPOSIT.doubleValue()); - private void registerAccounts(int number, int defaultMoney) { - for ( int i = 0; i < number; i++) { - bank.registerAccount(i, defaultMoney); + UUID accountInId = bank.getRandomBankAccountId(); + UUID accountOutId = bank.getRandomBankAccountId(); + while (accountInId.equals(accountOutId)) { + accountOutId = bank.getRandomBankAccountId(); } - } - - private void sanityCheck( int accountMaxNumber, int totalExpectedMoney) { - BigDecimal sum = IntStream.range(0, accountMaxNumber) - .mapToObj( bank::getAccount) - .map ( Account::getMoneyAsBigDecimal) - .reduce( BigDecimal.ZERO, BigDecimal::add); - if ( sum.intValue() != totalExpectedMoney) { - throw new IllegalStateException("we got "+ sum + " != " + totalExpectedMoney +" (expected)"); - } - System.out.println("sanity check OK"); + bank.transferMoney(accountOutId, accountInId, transferMoneyAmount); } - - } diff --git a/src/main/java/ch/engenius/bank/domain/Bank.java b/src/main/java/ch/engenius/bank/domain/Bank.java new file mode 100644 index 0000000..78c0614 --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/Bank.java @@ -0,0 +1,44 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exceptions.NotFoundException; +import lombok.*; + +import java.math.BigDecimal; +import java.util.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Bank { + + private HashMap accounts = new HashMap<>(); + + public void registerBankAccount(UUID accountNumber, BigDecimal initialMoneyAmount) { + BankAccount account = new BankAccount(); + account.setMoneyAmount(initialMoneyAmount); + account.setAccountNumber(accountNumber); + this.accounts.put(accountNumber, account); + } + + @Synchronized + public void transferMoney(UUID bankAccountOutId, UUID bankAccountInId, BigDecimal moneyAmount) { + BankAccount bankAccountOut = getBankAccount(bankAccountOutId); + BankAccount bankAccountIn = getBankAccount(bankAccountInId); + + bankAccountOut.withdraw(moneyAmount); + bankAccountIn.deposit(moneyAmount); + } + + public BankAccount getBankAccount(UUID accountNumber) { + if (this.accounts.get(accountNumber) == null) { + throw new NotFoundException("Bank account not found."); + } + return this.accounts.get(accountNumber); + } + + public UUID getRandomBankAccountId() { + List accountIds = new ArrayList<>(getAccounts().keySet()); + return accountIds.get(new Random().nextInt(accountIds.size())); + } +} diff --git a/src/main/java/ch/engenius/bank/domain/BankAccount.java b/src/main/java/ch/engenius/bank/domain/BankAccount.java new file mode 100644 index 0000000..bacb393 --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/BankAccount.java @@ -0,0 +1,30 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exceptions.NotEnoughCreditsException; +import lombok.*; + +import java.math.BigDecimal; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class BankAccount { + + private UUID accountNumber; + private BigDecimal moneyAmount; + + @Synchronized + public void withdraw(BigDecimal amount) { + if ((moneyAmount.compareTo(amount)) < 0) { + throw new NotEnoughCreditsException("Not enough credits on bank account."); + } + setMoneyAmount(moneyAmount.subtract(amount)); + } + + @Synchronized + public void deposit(BigDecimal amount) { + setMoneyAmount(moneyAmount.add(amount)); + } +} diff --git a/src/main/java/ch/engenius/bank/exceptions/BankMoneyAmountException.java b/src/main/java/ch/engenius/bank/exceptions/BankMoneyAmountException.java new file mode 100644 index 0000000..04d304f --- /dev/null +++ b/src/main/java/ch/engenius/bank/exceptions/BankMoneyAmountException.java @@ -0,0 +1,8 @@ +package ch.engenius.bank.exceptions; + +public class BankMoneyAmountException extends RuntimeException { + + public BankMoneyAmountException(String message) { + super(message); + } +} diff --git a/src/main/java/ch/engenius/bank/exceptions/NotEnoughCreditsException.java b/src/main/java/ch/engenius/bank/exceptions/NotEnoughCreditsException.java new file mode 100644 index 0000000..a1d03a5 --- /dev/null +++ b/src/main/java/ch/engenius/bank/exceptions/NotEnoughCreditsException.java @@ -0,0 +1,8 @@ +package ch.engenius.bank.exceptions; + +public class NotEnoughCreditsException extends RuntimeException { + + public NotEnoughCreditsException(String message) { + super(message); + } +} diff --git a/src/main/java/ch/engenius/bank/exceptions/NotFoundException.java b/src/main/java/ch/engenius/bank/exceptions/NotFoundException.java new file mode 100644 index 0000000..0610a84 --- /dev/null +++ b/src/main/java/ch/engenius/bank/exceptions/NotFoundException.java @@ -0,0 +1,8 @@ +package ch.engenius.bank.exceptions; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/test/ch/engenius/bank/domain/BankAccountTest.java b/src/main/test/ch/engenius/bank/domain/BankAccountTest.java new file mode 100644 index 0000000..b501574 --- /dev/null +++ b/src/main/test/ch/engenius/bank/domain/BankAccountTest.java @@ -0,0 +1,47 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exceptions.NotEnoughCreditsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BankAccountTest { + + private static final BigDecimal DEFAULT_MONEY_AMOUNT_TEN = BigDecimal.TEN; + private static final BigDecimal WITHDRAW_MONEY_AMOUNT_FIVE = BigDecimal.valueOf(5); + private static final BigDecimal MONEY_DEPOSIT = BigDecimal.TEN; + private final BankAccount bankAccount = new BankAccount(); + + @BeforeEach + public void setUp() { + bankAccount.setAccountNumber(UUID.randomUUID()); + bankAccount.setMoneyAmount(DEFAULT_MONEY_AMOUNT_TEN); + } + + @Test + public void shouldThrowNotEnoughCreditsExceptionWhenTryingToWithdraw() { + try { + bankAccount.withdraw(DEFAULT_MONEY_AMOUNT_TEN.multiply(BigDecimal.TEN)); + } catch (NotEnoughCreditsException e) { + assertEquals("Not enough credits on bank account.", e.getMessage()); + } + } + + @Test + public void shouldWithdrawSuccessfully() { + bankAccount.withdraw(WITHDRAW_MONEY_AMOUNT_FIVE); + + assertEquals(DEFAULT_MONEY_AMOUNT_TEN.subtract(WITHDRAW_MONEY_AMOUNT_FIVE), bankAccount.getMoneyAmount()); + } + + @Test + public void shouldDepositMoneySuccessfully() { + bankAccount.deposit(MONEY_DEPOSIT); + + assertEquals(DEFAULT_MONEY_AMOUNT_TEN.add(MONEY_DEPOSIT), bankAccount.getMoneyAmount()); + } +} diff --git a/src/main/test/ch/engenius/bank/domain/BankTest.java b/src/main/test/ch/engenius/bank/domain/BankTest.java new file mode 100644 index 0000000..c43600c --- /dev/null +++ b/src/main/test/ch/engenius/bank/domain/BankTest.java @@ -0,0 +1,93 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exceptions.NotEnoughCreditsException; +import ch.engenius.bank.exceptions.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class BankTest { + private static final UUID BANK_ACCOUNT_ONE_ID = UUID.nameUUIDFromBytes("first account".getBytes()); + private static final UUID BANK_ACCOUNT_TWO_ID = UUID.nameUUIDFromBytes("second account".getBytes()); + private static final BigDecimal DEFAULT_MONEY_AMOUNT_TEN = BigDecimal.TEN; + private static final BigDecimal TOO_BIG_AMOUNT_FOR_WITHDRAW = DEFAULT_MONEY_AMOUNT_TEN.multiply(DEFAULT_MONEY_AMOUNT_TEN); + + private final Bank bank = new Bank(); + + @BeforeEach + public void setUp() { + HashMap accounts = new HashMap<>(); + BankAccount bankAccountOne = new BankAccount(BANK_ACCOUNT_ONE_ID, DEFAULT_MONEY_AMOUNT_TEN); + BankAccount bankAccountTwo = new BankAccount(BANK_ACCOUNT_TWO_ID, DEFAULT_MONEY_AMOUNT_TEN); + accounts.put(BANK_ACCOUNT_ONE_ID, bankAccountOne); + accounts.put(BANK_ACCOUNT_TWO_ID, bankAccountTwo); + bank.setAccounts(accounts); + } + + @Test + public void shouldRegisterNewBankAccountSuccessfully() { + UUID bankAccountIdExpected = UUID.randomUUID(); + BigDecimal bankAccountAmountExpected = BigDecimal.TEN; + bank.registerBankAccount(bankAccountIdExpected, bankAccountAmountExpected); + + BankAccount actualBankAccount = bank.getAccounts().get(bankAccountIdExpected); + assertNotNull(actualBankAccount); + assertEquals(bankAccountIdExpected, actualBankAccount.getAccountNumber()); + assertEquals(bankAccountAmountExpected, actualBankAccount.getMoneyAmount()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenBankAccountWithProvidedIdDoesNotExistWhenTransferingMoney() { + try { + bank.transferMoney(BANK_ACCOUNT_ONE_ID, UUID.randomUUID(), BigDecimal.ONE); + } catch (NotFoundException e) { + assertEquals("Bank account not found.", e.getMessage()); + } + } + + @Test + public void shouldThrowNotEnoughCreditsExceptionWhenBankAccountWithProvidedIdDoesNotExistWhenTransferingMoney() { + try { + bank.transferMoney(BANK_ACCOUNT_ONE_ID, BANK_ACCOUNT_TWO_ID, TOO_BIG_AMOUNT_FOR_WITHDRAW); + } catch (NotEnoughCreditsException e) { + assertEquals("Not enough credits on bank account.", e.getMessage()); + } + } + + @Test + public void shouldTransferMoneySuccessfully() { + BigDecimal moneyAmount = BigDecimal.ONE; + bank.transferMoney(BANK_ACCOUNT_ONE_ID, BANK_ACCOUNT_TWO_ID, BigDecimal.ONE); + + BankAccount bankAccountOne = bank.getAccounts().get(BANK_ACCOUNT_ONE_ID); + BankAccount bankAccountTwo = bank.getAccounts().get(BANK_ACCOUNT_TWO_ID); + + assertEquals(0, bankAccountOne.getMoneyAmount().compareTo(DEFAULT_MONEY_AMOUNT_TEN.subtract(moneyAmount))); + assertEquals(0, bankAccountTwo.getMoneyAmount().compareTo(DEFAULT_MONEY_AMOUNT_TEN.add(moneyAmount))); + } + + @Test + public void shouldThrowNotFoundExceptionWhenBankAccountWithProvidedIdDoesNotExist() { + try { + bank.getBankAccount(UUID.randomUUID()); + } catch (NotFoundException e) { + assertEquals("Bank account not found.", e.getMessage()); + } + } + + @Test + public void shouldReturnAccountSuccessfully() { + BankAccount bankAccountExpected = bank.getAccounts().get(BANK_ACCOUNT_ONE_ID); + + BankAccount actualBankAccount = bank.getBankAccount(BANK_ACCOUNT_ONE_ID); + assertNotNull(actualBankAccount); + assertEquals(bankAccountExpected.getAccountNumber(), actualBankAccount.getAccountNumber()); + assertEquals(bankAccountExpected.getMoneyAmount(), actualBankAccount.getMoneyAmount()); + } +}