Java ATM CLI Dev Log #2: Transfer Cash, Hanging?
Source: Dev.to
Narrative
I never expected building this feature to be so crazy for me. While developing it, everything was going smoothly—until the function started to hang during execution.
I did a deep dive with ChatGPT and Gemini to get to the bottom of the issue. In the process, I learned about COMMIT, ROLLBACK, and why transactions are crucial in databases—especially for financial applications like the one I’m building.
Transactions are a sequence of SQL statements that are treated as a single logical unit. A transaction starts with a data‑changing operation and ends with either
COMMIT(successful) orROLLBACK(error).
Initial Implementation
AccountService.java
public static void transferCash(Account sender, Account receiver, double amount) {
if (sender.getBalance() > amount) {
double newSenderBalance = sender.getBalance() - amount;
double newReceiverBalance = receiver.getBalance() + amount;
boolean isTransactionSuccessful = AccountDAO.makeTransfer(
sender.getId(),
receiver.getId(),
sender.getAccountType(),
receiver.getAccountType(),
newSenderBalance,
newReceiverBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
sender.setBalance(newSenderBalance);
System.out.println("Withdrawal successful. New balance: $" + sender.getBalance());
sender.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
} else {
System.out.println("Insufficient funds.");
}
}
AccountDAO.java
public static boolean makeTransfer(
int senderId,
int receiverId,
String senderAccountType,
String receiverAccountType,
double newSenderBalance,
double newReceiverBalance) {
String senderQuery = "UPDATE accounts SET balance = ? WHERE customerId = ? AND accountType = ?";
String receiverQuery = "UPDATE accounts SET balance = ? WHERE customerId = ? AND accountType = ?";
boolean isTransactionSuccessful = false;
try (Connection conn = DBHelper.getConnection()) {
conn.setAutoCommit(false);
// ---- Sender ----
try (PreparedStatement senderStmt = conn.prepareStatement(senderQuery)) {
senderStmt.setDouble(1, newSenderBalance);
senderStmt.setInt(2, senderId);
senderStmt.setString(3, senderAccountType);
int senderRowsAffected = senderStmt.executeUpdate();
if (senderRowsAffected == 0) {
conn.rollback();
return false;
}
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();
return false;
}
// ---- Receiver ----
try (PreparedStatement receiverStmt = conn.prepareStatement(receiverQuery)) {
receiverStmt.setDouble(1, newReceiverBalance);
receiverStmt.setInt(2, receiverId);
receiverStmt.setString(3, receiverAccountType);
int receiverRowsAffected = receiverStmt.executeUpdate();
if (receiverRowsAffected == 0) {
conn.rollback();
return false;
}
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();
return false;
}
conn.commit();
System.out.println("\nBalance Updated Successfully!");
isTransactionSuccessful = true;
} catch (Exception e) {
e.printStackTrace();
isTransactionSuccessful = false;
}
return isTransactionSuccessful;
}
Refactor After Senior Engineer Feedback
A senior engineer (with banking‑Java experience) indicated that the previous approach was bad practice.
The service layer has been split into two smaller, single‑purpose operations: debit and credit.
Updated AccountService.java
public class AccountService {
public static void debitAccount(Account account, double amount) {
if (account.getBalance() > amount) {
double newBalance = account.getBalance() - amount;
boolean isTransactionSuccessful = AccountDAO.changeBalance(
account.getId(),
account.getAccountType(),
newBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
account.setBalance(newBalance);
System.out.println("Withdrawal successful. New balance: $" + account.getBalance());
account.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
} else {
System.out.println("Insufficient funds.");
}
}
public static void creditAccount(Account account, double amount) {
double newBalance = account.getBalance() + amount;
boolean isTransactionSuccessful = AccountDAO.changeBalance(
account.getId(),
account.getAccountType(),
newBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
account.setBalance(newBalance);
System.out.println("Deposit successful. New balance: $" + account.getBalance());
account.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
}
}
Note:
AccountDAO.changeBalanceis a new helper that updates a single account’s balance.
Each DB operation is now atomic, allowing the caller to orchestrate a full transfer (debit → credit) within a proper transaction scope.
Current Status
After all these changes, I decided to pause work on this feature, finish the remaining parts of the application, and then come back later to fix the transfer logic properly.
Then, I must have gained some mastery in how to deal with transactions properly in Java.
If you want to check out the GitHub repo, you can click here and see it for yourself.
Well, that’s it for now.
Until the next commit, bye 👋