Java Banking Application
Table of Contents + β
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
Accountclass 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.
privatemeansaccount.balance = -5000;will not even compile frommain.- There is a getter for the balance but no setter. That is on purpose.
- So the only way to change the balance is through
depositandwithdraw, 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
depositchecks 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 <= 0first. If bad, it prints a message and returnsfalse, before touching the balance. - Only a valid amount gets added, and then it returns
true. - The
booleanresult 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
throwstops 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.
accountsis anArrayList<Account>. It grows as the user creates more accounts.scannerreads input.nextAccountNumberstarts 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 callsbreakto leave. - Each option calls one small method. One method, one job. So
mainstays 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
Accountenforces the rule. - The withdraw menu method wraps the call in
try/catch, becauseaccount.withdrawcan 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. ExitHere is what the terminal shows. The overdraw attempt is caught and reported, and the balance is never harmed.
Output
===== BANK MENU =====1. Create account2. Deposit3. Withdraw4. Check balance5. View all accounts6. ExitChoose an option: 1Enter holder name: AlexEnter opening balance: 1000β
Account created. Number: 1001
Choose an option: 1Enter holder name: RiyaEnter opening balance: 500β
Account created. Number: 1002
Choose an option: 2Enter account number: 1001Enter amount to deposit: 500β
Deposited 500.0. New balance: 1500.0
Choose an option: 3Enter account number: 1001Enter amount to withdraw: 300β
Withdrew 300.0. New balance: 1200.0
Choose an option: 3Enter account number: 1001Enter amount to withdraw: 5000β οΈ Cannot withdraw 5000.0; balance is only 1200.0
Choose an option: 5----- ALL ACCOUNTS -----1001 | Alex | 1200.01002 | Riya | 500.0
Choose an option: 6Goodbye!Look at the overdraw. The user tried to withdraw 5000 from an account holding 1200.
withdrawthrewInsufficientFundsException.- 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
depositandwithdraw. -
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
withdrawdeclaresthrows 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()ornextDouble(), a leftover newline sits in the buffer. If you then callnextLine()it reads that empty line instead of waiting. Callingscanner.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
nullfromfindAccountbefore using the account, or the program crashes with aNullPointerException.
β 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
mainshort 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
Why is the balance field made private?
Why: A private balance can only change through validated methods, keeping the account valid.
- 2
When should the deposit method check that the amount is positive?
Why: Validating before updating means an invalid deposit changes nothing.
- 3
Why use a custom InsufficientFundsException?
Why: A specific exception is self-documenting and catchable, so callers can handle the overdraw their way.
- 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)