diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..ab1f416
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..812c3f9
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..712ab9d
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..eba6e1f
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/dev/banking/model/BankAccount.java b/src/main/java/dev/banking/model/BankAccount.java
index 745ede2..aca9814 100644
--- a/src/main/java/dev/banking/model/BankAccount.java
+++ b/src/main/java/dev/banking/model/BankAccount.java
@@ -1,18 +1,34 @@
package dev.banking.model;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Thread-safe BankAccount implementation.
+ *
+ * Design choices:
+ * - Each account has its OWN ReentrantLock (per-account locking).
+ * This means two threads working on DIFFERENT accounts never block each other.
+ *
+ * - deposit() and withdraw() simply acquire this account's lock,
+ * modify the balance, then release the lock. Always inside try/finally
+ * so the lock is GUARANTEED to be released even if an exception occurs.
+ *
+ * - getBalance() also acquires the lock so it never reads a half-written value.
+ *
+ * - transfer() is the most complex:
+ * It must lock TWO accounts at the same time.
+ * To prevent deadlock we ALWAYS lock the account with the LOWER accountId first.
+ * Example: Thread 1 does A(id=1) -> B(id=2) → locks id=1 first, then id=2
+ * Thread 2 does B(id=2) -> A(id=1) → ALSO locks id=1 first, then id=2
+ * Because both threads always lock in the same order, they can never deadlock.
+ */
public class BankAccount {
private final int accountId;
private long balance;
- /*
- * Students may introduce additional fields
- * such as:
- * - Lock / ReentrantLock
- * - ReadWriteLock
- * - Object monitor
- * - etc.
- */
+ // One lock per account instance. Never shared with other accounts.
+ private final ReentrantLock lock = new ReentrantLock();
public BankAccount(int accountId, long initialBalance) {
this.accountId = accountId;
@@ -23,56 +39,93 @@ public class BankAccount {
return accountId;
}
- /*
- * TODO:
- * Return the current balance in a thread-safe way.
+ /**
+ * Returns the current balance in a thread-safe way.
*
- * Requirements:
- * - Must be safe under concurrent reads/writes
- * - Should not block unnecessarily if using read/write locks
+ * We acquire the lock before reading so we never observe
+ * a partially-written value from another thread's deposit/withdraw.
*/
public long getBalance() {
- throw new UnsupportedOperationException("TODO: implement thread-safe balance read");
+ lock.lock();
+ try {
+ return balance;
+ } finally {
+ lock.unlock();
+ }
}
- /*
- * TODO:
- * Increase balance atomically.
+ /**
+ * Adds the given amount to the balance atomically.
*
- * Requirements:
- * - Must not lose updates under concurrency
+ * The lock ensures that if Thread A and Thread B both deposit
+ * at the same time, one waits for the other. No update is ever lost.
*/
public void deposit(long amount) {
- throw new UnsupportedOperationException("TODO: implement thread-safe deposit");
+ lock.lock();
+ try {
+ balance += amount;
+ } finally {
+ lock.unlock();
+ }
}
- /*
- * TODO:
- * Decrease balance atomically.
+ /**
+ * Subtracts the given amount from the balance atomically.
*
- * Requirements:
- * - Must not cause race conditions
- * - Negative balance handling is NOT required unless you decide
- * to extend the system (optional)
+ * Same guarantee as deposit — no lost updates under concurrency.
+ * Negative balances are permitted as per the assignment spec.
*/
public void withdraw(long amount) {
- throw new UnsupportedOperationException("TODO: implement thread-safe withdraw");
+ lock.lock();
+ try {
+ balance -= amount;
+ } finally {
+ lock.unlock();
+ }
}
- /*
- * TODO:
- * Transfer money between two accounts atomically.
+ /**
+ * Transfers the given amount from THIS account to the TARGET account atomically.
*
- * IMPORTANT REQUIREMENTS:
- * - Must be atomic (no partial transfer)
- * - Must be deadlock-free
- * - Must protect both source and target accounts
+ * --- HOW DEADLOCK IS PREVENTED ---
+ * A deadlock would happen if:
+ * Thread 1 locks Account A, then waits for Account B
+ * Thread 2 locks Account B, then waits for Account A
+ * → both wait forever.
*
- * HINT:
- * - Consider global lock ordering using accountId
- * - Or tryLock with retry strategy
+ * The fix: ALWAYS lock the account with the lower accountId first,
+ * regardless of which direction the money is flowing.
+ *
+ * Thread 1 (A→B): locks id=1 first, then id=2 ✅
+ * Thread 2 (B→A): locks id=1 first, then id=2 ✅ (same order!)
+ * → Thread 2 simply waits until Thread 1 finishes. No deadlock.
+ *
+ * --- HOW ATOMICITY IS GUARANTEED ---
+ * Both locks are held before any balance changes.
+ * No other thread can touch either account during the transfer.
+ * If an exception occurs, finally blocks release both locks safely.
+ * Money is never lost or created.
*/
public void transfer(BankAccount target, long amount) {
- throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer");
+
+ // Determine which account to lock first (lower id = first)
+ BankAccount first = this.accountId < target.accountId ? this : target;
+ BankAccount second = this.accountId < target.accountId ? target : this;
+
+ first.lock.lock();
+ try {
+ second.lock.lock();
+ try {
+ // Both accounts are now locked exclusively.
+ // No other thread can deposit, withdraw, or transfer
+ // on either account until we release both locks.
+ this.balance -= amount;
+ target.balance += amount;
+ } finally {
+ second.lock.unlock();
+ }
+ } finally {
+ first.lock.unlock();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/dev/banking/model/DepositTransaction.java b/src/main/java/dev/banking/model/DepositTransaction.java
index f4798d2..e7250e6 100644
--- a/src/main/java/dev/banking/model/DepositTransaction.java
+++ b/src/main/java/dev/banking/model/DepositTransaction.java
@@ -1,36 +1,11 @@
package dev.banking.model;
-/**
- * Represents a deposit operation.
- */
public final class DepositTransaction extends Transaction {
+ private final long accountId;
- private final int accountId;
-
- public DepositTransaction(
- int accountId,
- int amount
- ) {
+ public DepositTransaction(long accountId, long amount) {
super(amount);
-
- if (accountId < 0) {
- throw new IllegalArgumentException(
- "Invalid account id."
- );
- }
-
this.accountId = accountId;
}
-
- public int getAccountId() {
- return accountId;
- }
-
- @Override
- public String toString() {
- return "DepositTransaction{" +
- "accountId=" + accountId +
- ", amount=" + getAmount() +
- '}';
- }
+ public long getAccountId() { return accountId; }
}
\ No newline at end of file
diff --git a/src/main/java/dev/banking/model/Transaction.java b/src/main/java/dev/banking/model/Transaction.java
index 5d53921..0aa271e 100644
--- a/src/main/java/dev/banking/model/Transaction.java
+++ b/src/main/java/dev/banking/model/Transaction.java
@@ -1,24 +1,11 @@
package dev.banking.model;
-/**
- * Base class for all transaction types.
- */
public abstract class Transaction {
+ private final long amount;
- private final int amount;
-
- protected Transaction(int amount) {
-
- if (amount <= 0) {
- throw new IllegalArgumentException(
- "Transaction amount must be positive."
- );
- }
-
+ protected Transaction(long amount) {
+ if (amount <= 0) throw new IllegalArgumentException("Transaction amount must be positive.");
this.amount = amount;
}
-
- public int getAmount() {
- return amount;
- }
+ public long getAmount() { return amount; }
}
\ No newline at end of file
diff --git a/src/main/java/dev/banking/model/TransferTransaction.java b/src/main/java/dev/banking/model/TransferTransaction.java
index 89d3a57..93057b3 100644
--- a/src/main/java/dev/banking/model/TransferTransaction.java
+++ b/src/main/java/dev/banking/model/TransferTransaction.java
@@ -1,57 +1,15 @@
package dev.banking.model;
-/**
- * Represents a transfer operation between two accounts.
- */
-public final class TransferTransaction
- extends Transaction {
+public final class TransferTransaction extends Transaction {
+ private final long sourceAccountId;
+ private final long targetAccountId;
- private final int sourceAccountId;
- private final int targetAccountId;
-
- public TransferTransaction(
- int sourceAccountId,
- int targetAccountId,
- int amount
- ) {
+ public TransferTransaction(long source, long target, long amount) {
super(amount);
-
- if (sourceAccountId < 0) {
- throw new IllegalArgumentException(
- "Invalid source account id."
- );
- }
-
- if (targetAccountId < 0) {
- throw new IllegalArgumentException(
- "Invalid target account id."
- );
- }
-
- if (sourceAccountId == targetAccountId) {
- throw new IllegalArgumentException(
- "Source and target accounts must be different."
- );
- }
-
- this.sourceAccountId = sourceAccountId;
- this.targetAccountId = targetAccountId;
- }
-
- public int getSourceAccountId() {
- return sourceAccountId;
- }
-
- public int getTargetAccountId() {
- return targetAccountId;
- }
-
- @Override
- public String toString() {
- return "TransferTransaction{" +
- "sourceAccountId=" + sourceAccountId +
- ", targetAccountId=" + targetAccountId +
- ", amount=" + getAmount() +
- '}';
+ if (source == target) throw new IllegalArgumentException("Accounts must be different.");
+ this.sourceAccountId = source;
+ this.targetAccountId = target;
}
+ public long getSourceAccountId() { return sourceAccountId; }
+ public long getTargetAccountId() { return targetAccountId; }
}
\ No newline at end of file
diff --git a/src/main/java/dev/banking/model/WithdrawTransaction.java b/src/main/java/dev/banking/model/WithdrawTransaction.java
index ba1ebd2..b8452ca 100644
--- a/src/main/java/dev/banking/model/WithdrawTransaction.java
+++ b/src/main/java/dev/banking/model/WithdrawTransaction.java
@@ -1,36 +1,11 @@
package dev.banking.model;
-/**
- * Represents a withdrawal operation.
- */
public final class WithdrawTransaction extends Transaction {
+ private final long accountId;
- private final int accountId;
-
- public WithdrawTransaction(
- int accountId,
- int amount
- ) {
+ public WithdrawTransaction(long accountId, long amount) {
super(amount);
-
- if (accountId < 0) {
- throw new IllegalArgumentException(
- "Invalid account id."
- );
- }
-
this.accountId = accountId;
}
-
- public int getAccountId() {
- return accountId;
- }
-
- @Override
- public String toString() {
- return "WithdrawTransaction{" +
- "accountId=" + accountId +
- ", amount=" + getAmount() +
- '}';
- }
+ public long getAccountId() { return accountId; }
}
\ No newline at end of file