DEV Community

Victor Chigbo
Victor Chigbo

Posted on

Java ATM CLI Dev Log #4: Final Entry

So this is the final entry for this project. This will cover the summary and what I've learnt so far from the project. I want to give a big shoutout to Marshall Odii for helping me with fixing the issues I had with the transfer method.

So this project was me taking a step further to learn Java and give myself basics of how financial systems work. Attention to detail is an important skill because you have to be sure of whatever you are doing. If you ever feel sleepy, drowsy or whatever while working on systems like these, the best thing for you to do in order to save yourself from a very big disgrace and loss to leave your laptop and take a nap.

It also opened my eyes to system design and software architecture. The project is obviously a monolith but within it, I applied a Domain-Driven Design to how it is structured and it helps me to navigate through the codebase, especially when I'm debugging it.

As for the fix, this is what he did. Recall how I previously wrote transferCash and makeTransfer in AccountService.java and AccountDAO.java respectively:

// 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.");
        } 
    }
Enter fullscreen mode Exit fullscreen mode
// 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);

            // For the sender account
            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;
            }

            // For the receiver account
            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;
    }
Enter fullscreen mode Exit fullscreen mode

This is he what he did:

// AccountService.java (Marshall)
public static void transferCash(Account senderAccount, Account recipientAccount, double amount) {
        // Solution: Validate sender has sufficient balance before attempting transfer
        if (senderAccount.getBalance() < amount) {
            System.out.println("Insufficient funds. Cannot complete transfer.");
            return;
        }

        // Solution: Calculate new balances
        double newSenderBalance = senderAccount.getBalance() - amount;
        double newRecipientBalance = recipientAccount.getBalance() + amount;

        // Solution: Use makeTransfer() which performs both updates in a single transaction
        // This prevents deadlocks that occur when using separate connections
        boolean isTransactionSuccessful = AccountDAO.makeTransfer(
            senderAccount.getId(),
            recipientAccount.getId(),
            senderAccount.getAccountType(),
            recipientAccount.getAccountType(),
            newSenderBalance,
            newRecipientBalance
        );

        if (isTransactionSuccessful) {
            // Solution: Update account objects only after successful database transaction
            senderAccount.setBalance(newSenderBalance);
            recipientAccount.setBalance(newRecipientBalance);

            System.out.println("\nTransfer Successful!");
            System.out.println("Amount transferred: $" + amount);
            System.out.println("\nSender Account:");
            System.out.println(senderAccount.stringifyAccount());
            System.out.println("\nRecipient Account:");
            System.out.println(recipientAccount.stringifyAccount());
        } else {
            System.out.println("Transfer Failed! Please try again.");
        }
    }
Enter fullscreen mode Exit fullscreen mode
// AccountDAO.java (Marshall)
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;

        // Solution: Fixed - Manual connection management to ensure proper rollback on exceptions
        // Using try-with-resources can cause issues because connection closes before rollback
        // Solution: Using a single connection for both updates prevents deadlocks
        Connection conn = null;
        try {
            conn = DbHelper.getConnection();
            // Solution: Disable auto-commit to enable transaction control
            // This ensures both updates happen atomically (all or nothing)
            conn.setAutoCommit(false);

            // For the sender account
            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) {
                    // Solution: Rollback transaction if sender update fails
                    conn.rollback();
                    return false;
                }
            }

            // For the receiver account
            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) {
                    // Solution: Rollback transaction if receiver update fails
                    conn.rollback();
                    return false;
                }
            }

            // Solution: Commit transaction only if both updates succeeded
            conn.commit();   
            System.out.println("\nBalance Updated Successfully!");
            isTransactionSuccessful = true;

        } catch (SQLException e) {
            // Solution: CRITICAL FIX - Explicitly rollback on exception to prevent hanging/deadlocks
            // If we don't rollback, the transaction remains open and can cause the database to hang
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException rollbackEx) {
                    System.err.println("Error during rollback: " + rollbackEx.getMessage());
                }
            }
            System.err.println("Error during transfer transaction: " + e.getMessage());
            e.printStackTrace();
            isTransactionSuccessful = false;
        } catch (Exception e) {
            // Solution: CRITICAL FIX - Explicitly rollback on exception to prevent hanging/deadlocks
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException rollbackEx) {
                    System.err.println("Error during rollback: " + rollbackEx.getMessage());
                }
            }
            System.err.println("Unexpected error during transfer: " + e.getMessage());
            e.printStackTrace();
            isTransactionSuccessful = false;
        } finally {
            // Solution: Ensure connection is always closed, even if rollback fails
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException closeEx) {
                    System.err.println("Error closing connection: " + closeEx.getMessage());
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

The explanations to the code blocks are in the comments, but basically, you first have to disable auto-commit in order to turn the queries into ACID transactions. Then, in two separate try blocks, you execute the updates for the sender and receiver accounts, and after that, you commit the transactions.

In summary, I learnt:

  1. JDBC
  2. Domain-Driven Design basics
  3. ACID
  4. Basics of operations in financial systems

So yeah. That's just it.

If you want to see the project, you can click here.

Until next time....bye! 👋

Top comments (0)