diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92322c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +target/ diff --git a/pom.xml b/pom.xml index 2ca15ec..dc4320c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,61 @@ ch.engenius accounts 1.0-SNAPSHOT + + UTF-8 + 1.8 + ${maven.compiler.source} + + + + + org.junit + junit-bom + 5.9.0 + pom + import + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.projectlombok + lombok + 1.18.24 + provided + + + org.slf4j + slf4j-api + 1.7.36 + + + org.slf4j + slf4j-simple + 1.7.36 + + + jakarta.validation + jakarta.validation-api + 3.0.2 + - + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + \ 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..2f49e43 100644 --- a/src/main/java/ch/engenius/bank/BankRunner.java +++ b/src/main/java/ch/engenius/bank/BankRunner.java @@ -1,71 +1,79 @@ package ch.engenius.bank; +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.domain.Bank; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; +import lombok.extern.slf4j.Slf4j; + import java.math.BigDecimal; import java.util.Random; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.stream.IntStream; +@Slf4j public class BankRunner { - + private static final int DEFAULT_DEPOSIT = 1000; + private static final int ITERATIONS = 10000; + private static final int ACCOUNTS = 100; private static final ExecutorService executor = Executors.newFixedThreadPool(8); - private final Random random = new Random(43); - private final Bank bank = new Bank(); + private final Bank bank; + public BankRunner() { + ConcurrentMap accounts = new ConcurrentHashMap<>(); + 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(ACCOUNTS, DEFAULT_DEPOSIT); + runner.sanityCheck(ACCOUNTS, ACCOUNTS * DEFAULT_DEPOSIT); + runner.runBank(ITERATIONS, ACCOUNTS); + runner.sanityCheck(ACCOUNTS, ACCOUNTS * DEFAULT_DEPOSIT); } private void runBank(int iterations, int maxAccount) { - for (int i =0; i< iterations; i++ ) { - executor.submit( ()-> runRandomOperation(maxAccount)); + for (int i = 0; i < iterations; i++) { + executor.submit(() -> initiateMoneyTransfer(maxAccount)); } try { executor.shutdown(); - executor.awaitTermination(100,TimeUnit.SECONDS); + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } } catch (InterruptedException e) { - e.printStackTrace(); + log.error("InterruptedException occurred: ", e); + executor.shutdownNow(); + Thread.currentThread().interrupt(); } } - 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 initiateMoneyTransfer(int maxAccount) { + Money moneyAmount = new Money(BigDecimal.valueOf(random.nextDouble() * 100.0)); + AccountNumber accountInNumber = new AccountNumber(random.nextInt(maxAccount)); + AccountNumber accountOutNumber = new AccountNumber(random.nextInt(maxAccount)); + + bank.transferMoney(accountOutNumber, accountInNumber, moneyAmount); } - private void registerAccounts(int number, int defaultMoney) { - for ( int i = 0; i < number; i++) { - bank.registerAccount(i, defaultMoney); + private void registerAccounts(int numberOfAccounts, int defaultMoney) { + for (int accountNumber = 0; accountNumber < numberOfAccounts; accountNumber++) { + bank.registerAccount(new AccountNumber(accountNumber), new Money(BigDecimal.valueOf(defaultMoney))); } } - private void sanityCheck( int accountMaxNumber, int totalExpectedMoney) { + private void sanityCheck(int accountMaxNumber, int totalExpectedMoney) { BigDecimal sum = IntStream.range(0, accountMaxNumber) - .mapToObj( bank::getAccount) - .map ( Account::getMoneyAsBigDecimal) - .reduce( BigDecimal.ZERO, BigDecimal::add); + .mapToObj(a -> bank.getAccount(new AccountNumber(a))) + .map(Account::getMoney) + .reduce(new Money(BigDecimal.ZERO), Money::add).getAmount(); - if ( sum.intValue() != totalExpectedMoney) { - throw new IllegalStateException("we got "+ sum + " != " + totalExpectedMoney +" (expected)"); + if (sum.intValue() != totalExpectedMoney) { + throw new IllegalStateException("we got " + sum + " != " + totalExpectedMoney + " (expected)"); } - System.out.println("sanity check OK"); - } + log.info("Sanity check: OK"); + } } diff --git a/src/main/java/ch/engenius/bank/domain/Account.java b/src/main/java/ch/engenius/bank/domain/Account.java new file mode 100644 index 0000000..6917770 --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/Account.java @@ -0,0 +1,40 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exception.AccountException; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; +import lombok.NonNull; +import lombok.Synchronized; + +@NonNull +public class Account { + private Money money; + private final AccountNumber accountNumber; + + public Account(AccountNumber accountNumber, Money money) { + this.accountNumber = accountNumber; + this.money = money; + } + + @Synchronized + public void withdraw(Money amount) { + if (money.getAmount().compareTo(amount.getAmount()) < 0) { + throw new AccountException("Not enough credit on account"); + } + + this.money = money.subtract(amount); + } + + @Synchronized + public void deposit(Money amount) { + this.money = money.add(amount); + } + + public Money getMoney() { + return money; + } + + public AccountNumber getAccountNumber() { + return accountNumber; + } +} 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..d89280d --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/Bank.java @@ -0,0 +1,50 @@ +package ch.engenius.bank.domain; + +import ch.engenius.bank.exception.BankException; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; +import ch.engenius.bank.service.BankService; +import lombok.Synchronized; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class Bank implements BankService { + private final ConcurrentMap accounts; + + public Bank() { + this(new ConcurrentHashMap<>()); + } + + public Bank(ConcurrentMap accounts) { + this.accounts = accounts; + } + + @Override + public Account registerAccount(AccountNumber accountNumber, Money amount) { + if (accounts.containsKey(accountNumber)) { + throw new BankException("Account already exists"); + } + + Account account = new Account(accountNumber, amount); + accounts.put(accountNumber, account); + return account; + } + + @Synchronized + public void transferMoney(AccountNumber accountOutNumber, AccountNumber accountInNumber, Money moneyAmount) { + Account payerAccount = getAccount(accountOutNumber); + Account payeeAccount = getAccount(accountInNumber); + + payerAccount.withdraw(moneyAmount); + payeeAccount.deposit(moneyAmount); + } + + @Override + public Account getAccount(AccountNumber accountNumber) { + if (accounts.get(accountNumber) == null) { + throw new BankException("Account not found"); + } + return accounts.get(accountNumber); + } +} diff --git a/src/main/java/ch/engenius/bank/exception/AccountException.java b/src/main/java/ch/engenius/bank/exception/AccountException.java new file mode 100644 index 0000000..c5dd980 --- /dev/null +++ b/src/main/java/ch/engenius/bank/exception/AccountException.java @@ -0,0 +1,8 @@ +package ch.engenius.bank.exception; + +public class AccountException extends RuntimeException { + + public AccountException(String message) { + super(message); + } +} diff --git a/src/main/java/ch/engenius/bank/exception/BankException.java b/src/main/java/ch/engenius/bank/exception/BankException.java new file mode 100644 index 0000000..5e84a9e --- /dev/null +++ b/src/main/java/ch/engenius/bank/exception/BankException.java @@ -0,0 +1,8 @@ +package ch.engenius.bank.exception; + +public class BankException extends RuntimeException { + + public BankException(String message) { + super(message); + } +} diff --git a/src/main/java/ch/engenius/bank/model/AccountNumber.java b/src/main/java/ch/engenius/bank/model/AccountNumber.java new file mode 100644 index 0000000..c2a40ac --- /dev/null +++ b/src/main/java/ch/engenius/bank/model/AccountNumber.java @@ -0,0 +1,15 @@ +package ch.engenius.bank.model; + +import jakarta.validation.constraints.PositiveOrZero; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +@Value +@RequiredArgsConstructor +public class AccountNumber { + + @NonNull + @PositiveOrZero + private int number; +} diff --git a/src/main/java/ch/engenius/bank/model/Money.java b/src/main/java/ch/engenius/bank/model/Money.java new file mode 100644 index 0000000..6c196a6 --- /dev/null +++ b/src/main/java/ch/engenius/bank/model/Money.java @@ -0,0 +1,25 @@ +package ch.engenius.bank.model; + +import jakarta.validation.constraints.PositiveOrZero; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import java.math.BigDecimal; + +@Value +@RequiredArgsConstructor +public class Money { + + @NonNull + @PositiveOrZero + private BigDecimal amount; + + public Money subtract(Money value) { + return new Money(amount.subtract(value.getAmount())); + } + + public Money add(Money value) { + return new Money(amount.add(value.getAmount())); + } +} diff --git a/src/main/java/ch/engenius/bank/service/BankService.java b/src/main/java/ch/engenius/bank/service/BankService.java new file mode 100644 index 0000000..ceed3d9 --- /dev/null +++ b/src/main/java/ch/engenius/bank/service/BankService.java @@ -0,0 +1,13 @@ +package ch.engenius.bank.service; + +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.exception.BankException; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; + +public interface BankService { + + Account registerAccount(AccountNumber accountNumber, Money amount) throws BankException; + + Account getAccount(AccountNumber accountNumber) throws BankException; +} diff --git a/src/test/java/ch/engenius/bank/AccountTest.java b/src/test/java/ch/engenius/bank/AccountTest.java new file mode 100644 index 0000000..655c624 --- /dev/null +++ b/src/test/java/ch/engenius/bank/AccountTest.java @@ -0,0 +1,40 @@ +package ch.engenius.bank; + +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AccountTest { + private Account account; + private final Money initialAccountMoney = new Money(BigDecimal.valueOf(100)); + private final AccountNumber defaultAccountNumber = new AccountNumber(1); + + @BeforeEach + public void setUp() { + account = new Account(defaultAccountNumber, initialAccountMoney); + } + + @Test + void shouldWithdrawMoney_whenAmountProvided() { + final BigDecimal withdrawAmount = BigDecimal.valueOf(10); + Money withdrawMoney = new Money(withdrawAmount); + + account.withdraw(withdrawMoney); + assertEquals(initialAccountMoney.getAmount().subtract(withdrawMoney.getAmount()), account.getMoney().getAmount()); + } + + @Test + void shouldDepositMoney_whenAmountProvided() { + final BigDecimal depositAmount = BigDecimal.valueOf(10); + Money depositMoney = new Money(depositAmount); + + account.deposit(depositMoney); + assertEquals(initialAccountMoney.getAmount().add(depositMoney.getAmount()), account.getMoney().getAmount()); + } +} \ No newline at end of file diff --git a/src/test/java/ch/engenius/bank/BankTest.java b/src/test/java/ch/engenius/bank/BankTest.java new file mode 100644 index 0000000..3621d43 --- /dev/null +++ b/src/test/java/ch/engenius/bank/BankTest.java @@ -0,0 +1,62 @@ +package ch.engenius.bank; + +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.domain.Bank; +import ch.engenius.bank.model.AccountNumber; +import ch.engenius.bank.model.Money; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class BankTest { + private Bank bank; + private Money initialAccountMoney; + + @BeforeEach + public void setUp() { + bank = new Bank(); + initialAccountMoney = new Money(BigDecimal.valueOf(100)); + } + + @Test + void shouldRegisterAccount_whenAccountNumberAndMoneyProvided() { + AccountNumber accountNumber = new AccountNumber(1); + + assertNotNull(bank.registerAccount(accountNumber, initialAccountMoney)); + } + + @Test + void shouldTransferMoney_whenPayerAccountAndPayeeAccountAndMoneyProvided() { + Money transferMoney = new Money(BigDecimal.valueOf(20)); + + AccountNumber payerAccountNumber = new AccountNumber(1); + bank.registerAccount(payerAccountNumber, initialAccountMoney); + + AccountNumber payeeAccountNumber = new AccountNumber(2); + bank.registerAccount(payeeAccountNumber, initialAccountMoney); + + bank.transferMoney(payerAccountNumber, payeeAccountNumber, transferMoney); + + Account payer = bank.getAccount(payerAccountNumber); + Account payee = bank.getAccount(payeeAccountNumber); + + assertEquals(payer.getMoney().getAmount(), + initialAccountMoney.getAmount().subtract(transferMoney.getAmount())); + assertEquals(payee.getMoney().getAmount(), + initialAccountMoney.getAmount().add(transferMoney.getAmount())); + } + + @Test + void shouldGetAccount_whenAccountNumberProvided() { + AccountNumber accountNumber = new AccountNumber(1); + bank.registerAccount(accountNumber, initialAccountMoney); + + Account account = bank.getAccount(accountNumber); + + assertNotNull(account); + } +} \ No newline at end of file diff --git a/src/test/java/ch/engenius/bank/model/MoneyTest.java b/src/test/java/ch/engenius/bank/model/MoneyTest.java new file mode 100644 index 0000000..865bf7d --- /dev/null +++ b/src/test/java/ch/engenius/bank/model/MoneyTest.java @@ -0,0 +1,30 @@ +package ch.engenius.bank.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MoneyTest { + + private Money initialMoney; + private Money amount; + + @BeforeEach + public void setUp() { + initialMoney = new Money(BigDecimal.valueOf(100)); + amount = new Money(BigDecimal.valueOf(30)); + } + + @Test + void shouldSubtract_whenAmountProvided() { + assertEquals(initialMoney.subtract(amount), new Money(BigDecimal.valueOf(70))); + } + + @Test + void shouldAdd_whenAmountProvided() { + assertEquals(initialMoney.add(amount), new Money(BigDecimal.valueOf(130))); + } +} \ No newline at end of file