Java Banking Application

In the last lesson you built a Java library management system. Now let’s build something with real rules: a Banking System. It is the perfect place to show off encapsulation, validation, and a custom exception. Let’s build it step by step.

🎯 What we are building

A Banking System manages accounts and their money. We will build a console app:

  • A console app is a text program that runs in the terminal.
  • The user picks menu options and types numbers.
  • The app prints the results back.

Here is what our app will do.

  • Create a new account.
  • Deposit money, only positive amounts.
  • Withdraw money, only if there are enough funds.
  • Check the balance of one account.
  • View all accounts at once.

The key word is safety. The money in an account must always be valid.

  • It can never go negative.
  • It can never grow from a fake deposit.
  • So we hide the balance and let only trusted methods touch it.

That idea is called encapsulation. Think of a real bank:

  • You never reach behind the counter and change your own balance.
  • You hand over a request, and the bank’s rules decide.
  • Our Account class is that counter.

πŸ”’ Step 1: an encapsulated Account class

Let’s start with the thing that holds the money. Every account needs a number, a holder name, and a balance.

class Account {
private int accountNumber; // βœ… private so nobody outside can change it
private String holderName;
private double balance; // βœ… private balance is the heart of safety
public Account(int accountNumber, String holderName, double openingBalance) {
this.accountNumber = accountNumber;
this.holderName = holderName;
this.balance = openingBalance;
}
public int getAccountNumber() {
return accountNumber;
}
public String getHolderName() {
return holderName;
}
public double getBalance() {
return balance;
}
}

Here is how that class works.

  • All three fields are private. The constructor sets them once.
  • The getters let outside code read the values but not change them.
  • private means account.balance = -5000; will not even compile from main.
  • There is a getter for the balance but no setter. That is on purpose.
  • So the only way to change the balance is through deposit and withdraw, which carry the rules.

This is encapsulation: keep the data private, expose only safe ways to use it. The account is in charge of its own money.

Why no setBalance method?

A setBalance method would quietly undo all our safety. Anyone could call account.setBalance(999999) and invent money, or set a negative balance. By leaving it out, the only paths to change the balance are deposit and withdraw, and both of those check the rules first. Fewer doors means fewer ways to break in.

βž• Step 2: deposit with validation

A deposit adds money. But not any number is allowed.

  • A deposit of zero does nothing useful.
  • A negative deposit would secretly drain the account.
  • So deposit checks the amount before it changes anything.
public boolean deposit(double amount) {
if (amount <= 0) {
System.out.println("❌ Deposit must be positive.");
return false; // reject and change nothing
}
balance = balance + amount;
System.out.println("βœ… Deposited " + amount + ". New balance: " + balance);
return true;
}

Here is what this method does.

  • It tests amount <= 0 first. If bad, it prints a message and returns false, before touching the balance.
  • Only a valid amount gets added, and then it returns true.
  • The boolean result tells the caller what happened, instead of guessing.
  • The rule lives inside the account. So whoever calls deposit, the same check applies.

That is the real lesson. Rules inside the object cannot be forgotten or skipped by careless calling code.

βž– Step 3: withdraw with a custom exception

Withdrawing is where things get interesting. Two things can go wrong.

  • The amount might not be positive. Same as deposit.
  • There might not be enough money. This is a real banking rule worth signalling loudly.

For the second case we use a custom exception. An exception is Java’s way of saying β€œsomething went wrong, stop normal flow.” A custom one is just an error we name ourselves so it reads clearly.

class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message); // pass the message up to the Exception parent
}
}

This class extends Exception, so it is a checked exception.

  • Checked means the compiler forces callers to catch it or declare it.
  • We want that here. Running out of money is a normal situation the caller must handle on purpose.

Now the withdraw method uses it.

public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
System.out.println("❌ Withdrawal must be positive.");
return;
}
if (amount > balance) {
throw new InsufficientFundsException(
"Cannot withdraw " + amount + "; balance is only " + balance);
}
balance = balance - amount; // βœ… only valid withdrawals reach here
System.out.println("βœ… Withdrew " + amount + ". New balance: " + balance);
}

Here is what makes this design strong.

  • It rejects a non-positive amount first, just like deposit.
  • If amount > balance, it throws the custom exception with the real numbers in the message.
  • The signature says throws InsufficientFundsException. That is a promise: β€œI might fail this way, be ready.”
  • A throw stops the method instantly. So the balance is never touched on a failed withdrawal.
  • The only line that subtracts money sits behind both guards. So the account can never go negative.

Why throw instead of just printing?

Printing β€œnot enough money” buries the problem inside the account. Throwing hands the decision back to the caller. The caller can show a friendly message, log the attempt, offer a smaller amount, or block the transaction. That choice belongs to the calling code, not to the account. Custom exceptions for business rules are common in real systems for exactly this reason.

🏦 Step 4: the menu loop and per-operation methods

We have a safe Account. Now we need a program that runs many of them. We store accounts in an ArrayList<Account>, show a menu, and loop until the user exits.

import java.util.ArrayList;
import java.util.Scanner;
public class BankingSystem {
static ArrayList<Account> accounts = new ArrayList<>();
static Scanner scanner = new Scanner(System.in);
static int nextAccountNumber = 1001; // auto-generate account numbers
public static void main(String[] args) {
while (true) {
System.out.println("\n===== BANK MENU =====");
System.out.println("1. Create account");
System.out.println("2. Deposit");
System.out.println("3. Withdraw");
System.out.println("4. Check balance");
System.out.println("5. View all accounts");
System.out.println("6. Exit");
System.out.print("Choose an option: ");
int choice = scanner.nextInt();
if (choice == 1) createAccount();
else if (choice == 2) deposit();
else if (choice == 3) withdraw();
else if (choice == 4) checkBalance();
else if (choice == 5) viewAll();
else if (choice == 6) {
System.out.println("Goodbye!");
break; // leave the loop and end the program
}
else System.out.println("❌ Invalid option. Try again.");
}
}
}

Here is the design.

  • accounts is an ArrayList<Account>. It grows as the user creates more accounts.
  • scanner reads input. nextAccountNumber starts at 1001 and goes up by one, so every account gets a unique number.
  • The while (true) loop shows the menu again and again. Option 6 calls break to leave.
  • Each option calls one small method. One method, one job. So main stays short.

Now the per-operation methods. We start with creating an account.

static void createAccount() {
scanner.nextLine(); // clear the leftover newline before reading text
System.out.print("Enter holder name: ");
String name = scanner.nextLine();
System.out.print("Enter opening balance: ");
double opening = scanner.nextDouble();
Account account = new Account(nextAccountNumber, name, opening);
accounts.add(account);
System.out.println("βœ… Account created. Number: " + nextAccountNumber);
nextAccountNumber++; // move on to the next number
}

This reads a name and opening balance, builds an Account, and adds it to the list. The scanner.nextLine() at the top clears a leftover newline. More on that trap in Common Mistakes.

To deposit or withdraw, we first need to find the right account by number. One helper does that everywhere.

static Account findAccount(int number) {
for (Account account : accounts) {
if (account.getAccountNumber() == number) {
return account; // found it
}
}
return null; // no account with that number
}

It returns the matching account, or null if there is none. The null lets each caller check for β€œnot found” and react. Now deposit and withdraw both use this helper.

static void deposit() {
System.out.print("Enter account number: ");
int number = scanner.nextInt();
Account account = findAccount(number);
if (account == null) {
System.out.println("❌ No account with that number.");
return;
}
System.out.print("Enter amount to deposit: ");
double amount = scanner.nextDouble();
account.deposit(amount); // the account enforces its own rule
}
static void withdraw() {
System.out.print("Enter account number: ");
int number = scanner.nextInt();
Account account = findAccount(number);
if (account == null) {
System.out.println("❌ No account with that number.");
return;
}
System.out.print("Enter amount to withdraw: ");
double amount = scanner.nextDouble();
try {
account.withdraw(amount);
} catch (InsufficientFundsException e) {
System.out.println("⚠️ " + e.getMessage()); // caller decides what to do
}
}

Notice the split of work.

  • The menu method talks to the user: find the account, read the amount.
  • The Account enforces the rule.
  • The withdraw menu method wraps the call in try/catch, because account.withdraw can throw.
  • When it throws, we catch it and show the message. So the account stays safe and the program keeps running.

Last, the two read-only views.

static void checkBalance() {
System.out.print("Enter account number: ");
int number = scanner.nextInt();
Account account = findAccount(number);
if (account == null) {
System.out.println("❌ No account with that number.");
return;
}
System.out.println("Balance for " + account.getHolderName()
+ ": " + account.getBalance());
}
static void viewAll() {
if (accounts.isEmpty()) {
System.out.println("No accounts yet.");
return;
}
System.out.println("----- ALL ACCOUNTS -----");
for (Account account : accounts) {
System.out.println(account.getAccountNumber()
+ " | " + account.getHolderName()
+ " | " + account.getBalance());
}
}

Both only read through getters, so they cannot break anything. viewAll checks for an empty list first, so we never print a header with nothing under it.

▢️ Step 5: a full sample run

Let’s trace a real session so you see every piece work together, including a rejected withdrawal.

// The user's actions during the run:
// 1. Create account for Alex, opening 1000
// 2. Create account for Riya, opening 500
// 3. Deposit 500 into account 1001
// 4. Withdraw 300 from account 1001
// 5. Withdraw 5000 from account 1001 ❌ should be rejected
// 6. View all accounts
// 7. Exit

Here is what the terminal shows. The overdraw attempt is caught and reported, and the balance is never harmed.

Output

===== BANK MENU =====
1. Create account
2. Deposit
3. Withdraw
4. Check balance
5. View all accounts
6. Exit
Choose an option: 1
Enter holder name: Alex
Enter opening balance: 1000
βœ… Account created. Number: 1001
Choose an option: 1
Enter holder name: Riya
Enter opening balance: 500
βœ… Account created. Number: 1002
Choose an option: 2
Enter account number: 1001
Enter amount to deposit: 500
βœ… Deposited 500.0. New balance: 1500.0
Choose an option: 3
Enter account number: 1001
Enter amount to withdraw: 300
βœ… Withdrew 300.0. New balance: 1200.0
Choose an option: 3
Enter account number: 1001
Enter amount to withdraw: 5000
⚠️ Cannot withdraw 5000.0; balance is only 1200.0
Choose an option: 5
----- ALL ACCOUNTS -----
1001 | Alex | 1200.0
1002 | Riya | 500.0
Choose an option: 6
Goodbye!

Look at the overdraw. The user tried to withdraw 5000 from an account holding 1200.

  • withdraw threw InsufficientFundsException.
  • The menu method caught it and printed the message.
  • The balance stayed at 1200.0, as the final view proves.

That is the whole safety promise paying off.

πŸ› οΈ Practice Challenge

Try extending the project before checking the answers. Each builds on methods you already have.

Challenge 1: Add a transfer(Account to, double amount) method on Account that moves money from this account to another, only if there are enough funds.

Challenge 2: Add a menu option that closes an account, but only if its balance is zero.

⚠️ Common Mistakes

A few banking-project slip-ups to watch for.

  • Leaving the balance public. A public balance can be set to anything, breaking every rule. Keep it private and change it only through deposit and withdraw.

  • Validating after changing the data. Always check the amount before you touch the balance. If you add first and check later, an invalid value has already done its damage.

  • Ignoring the checked exception. Since withdraw declares throws InsufficientFundsException, every caller must catch it or declare it. Forgetting this is a compile error, which is Java protecting you.

  • The Scanner newline trap. After nextInt() or nextDouble(), a leftover newline sits in the buffer. If you then call nextLine() it reads that empty line instead of waiting. Calling scanner.nextLine() once to clear it before reading text fixes the problem.

  • Forgetting the β€œnot found” case. When you look up an account by number, it might not exist. Always check for null from findAccount before using the account, or the program crashes with a NullPointerException.

βœ… Best Practices

Habits for safe data projects.

  • Encapsulate the data. Make the balance private and enforce rules in methods. No public field, no setter.
  • Validate first. Reject bad input before touching the real data, so a failed operation changes nothing.
  • Use a custom exception for business rules. It names the error clearly and lets the caller decide how to handle it.
  • Give each menu option its own method. One method per job keeps main short and each piece easy to test and read.
  • Reuse methods. Build transfer on top of withdraw and deposit, so the validation lives in one place only.

🧩 What You’ve Learned

Nicely done. Let’s recap the banking project.

  • βœ… A Banking System must protect its balance, which makes it a perfect case for encapsulation.
  • βœ… The balance is private with no setter; deposits and withdrawals go through methods that validate first.
  • βœ… A custom exception (InsufficientFundsException) reports an overdraw clearly and is catchable.
  • βœ… An ArrayList<Account> holds many accounts, and a menu loop lets the user create, deposit, withdraw, and view them.
  • βœ… The throw-and-catch pattern lets the caller handle a failed withdrawal gracefully, while the account stays safe.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why is the balance field made private?

    Why: A private balance can only change through validated methods, keeping the account valid.

  2. 2

    When should the deposit method check that the amount is positive?

    Why: Validating before updating means an invalid deposit changes nothing.

  3. 3

    Why use a custom InsufficientFundsException?

    Why: A specific exception is self-documenting and catchable, so callers can handle the overdraw their way.

  4. 4

    What is the cleanest way to build a transfer method?

    Why: Building transfer on withdraw and deposit reuses their rules, avoiding duplicated validation.

πŸš€ What’s Next?

Congratulations! You have reached the end of the Java course. You went from your very first program all the way to a full banking application with encapsulation, validation, and a custom exception. That is real progress.

The best way to lock it in is to keep building. Extend these projects. Add transfers, account closing, interest, or a transaction history. Try rewriting one project from memory. Each time, the ideas sink in deeper.

When you want to refresh the basics, jump back to the start of the course.

What is Java? (revisit the basics)

Share & Connect