Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea/
target/
57 changes: 56 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@
<groupId>ch.engenius</groupId>
<artifactId>accounts</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.9.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>


</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
31 changes: 0 additions & 31 deletions src/main/java/ch/engenius/bank/Account.java

This file was deleted.

18 changes: 0 additions & 18 deletions src/main/java/ch/engenius/bank/Bank.java

This file was deleted.

82 changes: 45 additions & 37 deletions src/main/java/ch/engenius/bank/BankRunner.java
Original file line number Diff line number Diff line change
@@ -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<AccountNumber, Account> 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");
}

}
40 changes: 40 additions & 0 deletions src/main/java/ch/engenius/bank/domain/Account.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
50 changes: 50 additions & 0 deletions src/main/java/ch/engenius/bank/domain/Bank.java
Original file line number Diff line number Diff line change
@@ -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<AccountNumber, Account> accounts;

public Bank() {
this(new ConcurrentHashMap<>());
}

public Bank(ConcurrentMap<AccountNumber, Account> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ch.engenius.bank.exception;

public class AccountException extends RuntimeException {

public AccountException(String message) {
super(message);
}
}
8 changes: 8 additions & 0 deletions src/main/java/ch/engenius/bank/exception/BankException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ch.engenius.bank.exception;

public class BankException extends RuntimeException {

public BankException(String message) {
super(message);
}
}
15 changes: 15 additions & 0 deletions src/main/java/ch/engenius/bank/model/AccountNumber.java
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 25 additions & 0 deletions src/main/java/ch/engenius/bank/model/Money.java
Original file line number Diff line number Diff line change
@@ -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()));
}
}
Loading