Skip to Content
Course content

40: Practice Exercise: Building a Class-Based Bank Account System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a pull request for a junior dev who was building a simple ledger system. He had created a BankAccount class, but he made the balance field public because he said it made the unit tests "easier to write" since he could just set the balance directly. Two days later, he came to my desk panicked because a different module in the app was accidentally setting account balances to negative values during a weird edge case in the currency conversion logic. Because the field was public, there was no "gatekeeper" to stop the data from being corrupted.

That is the quintessential lesson in encapsulation. When you're building a system like a bank account, the class shouldn't just be a container for data; it should be the sole authority on how that data is allowed to change. If you let any other class reach in and touch the balance, you've lost control of your business logic.

Shielding the Balance from External Interference

In a real-world Java application, you almost never want your internal state to be public. By marking your balance as private, you're essentially telling the rest of the program, "You can't touch this directly; you have to ask me to do it for you." This is where getters and setters come in, though in a banking system, a generic setBalance() method is actually a dangerous anti-pattern. You don't "set" a balance; you deposit or withdraw money.

public class BankAccount {
    private double balance;
    private String accountHolder;

    public BankAccount(String accountHolder, double initialBalance) {
        this.accountHolder = accountHolder;
        this.balance = initialBalance;
    }

    public double getBalance() {
        return balance;
    }
}

By providing a getter but no setter, the balance becomes read-only to the outside world. This is exactly how you prevent the "negative balance bug" I mentioned earlier. The only way the balance changes is through methods we explicitly define.

Encoding Banking Rules into Methods

Now that the data is shielded, we need to create controlled entry points. This is where we bake our business rules directly into the class. For instance, you can't deposit a negative amount of money—that's just a withdrawal in disguise, and it should be handled by a separate method. Similarly, you shouldn't be able to withdraw more than you actually have (unless you're implementing an overdraft feature, but let's keep it simple for now).

I always recommend throwing an error or returning a boolean to indicate if an operation failed. It forces the developer using your class to actually handle the failure instead of just assuming the money moved.

public void deposit(double amount) {
    if (amount > 0) {
        balance += amount;
        System.out.println("Deposited: $" + amount);
    } else {
        System.out.println("Invalid deposit amount.");
    }
}

public boolean withdraw(double amount) {
    if (amount > 0 && amount <= balance) {
        balance -= amount;
        System.out.println("Withdrew: $" + amount);
        return true;
    } else {
        System.out.println("Insufficient funds or invalid amount.");
        return false;
    }
}

Orchestrating the Account Logic

Once your BankAccount class is airtight, you need a driver class to put it to the test. This is where you'll instantiate your objects and simulate real-world usage. I like to think of this as the "user story" phase. You aren't just testing if the code runs; you're testing if the behavior matches reality. What happens if a user tries to withdraw $1,000 from a $500 account? Does the system crash, or does it gracefully deny the request?

When you write your main method, try to push the boundaries. Don't just do a successful deposit and withdrawal. Try to break the logic. If you can't find a way to force the balance into a negative state using the public methods, then you've successfully encapsulated your class.




📋 Practical Task

Exercise: The Robust Bank Account Simulator

Your task is to build a fully encapsulated banking system. You will need to create two files: BankAccount.java and BankSystem.java.

Requirements for BankAccount.java:

  • Create a private balance (double) and a private accountNumber (String).
  • Implement a constructor that initializes both fields.
  • Create a deposit(double amount) method that only adds money if the amount is positive.
  • Create a withdraw(double amount) method that only deducts money if the amount is positive and doesn't exceed the current balance. It should return true if successful and false otherwise.
  • Create a getBalance() method to allow read-only access to the balance.

Requirements for BankSystem.java:

  • In the main method, instantiate a BankAccount with an initial balance of $500.00.
  • Perform a sequence of operations:
    1. Deposit $200.
    2. Attempt to withdraw $1,000 (this should fail).
    3. Withdraw $300.
    4. Attempt to deposit a negative amount like -$50 (this should fail).
  • Print the final balance to the console to verify that only valid transactions were processed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.