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