Skip to content

Java Project: Bank Account System

Updated 18 min read Aimerscrew

A bank account program in Java is the best way to see why encapsulation matters. If any code could write balance = 1000000, nobody would trust the bank. So the balance stays private, and the only way to change it is through methods that check every request first. In this project you'll build accounts with deposits, withdrawals and transfers, a custom InsufficientFundsException, a transaction history and a menu that manages many accounts.

🌍 Real-life example

In a banking app, you never edit your balance directly. You ask for an action: "send $150 to James". The bank checks you have the money, takes it from your account, adds it to his, and both statements get a new line. If you don't have enough, the app refuses and nothing changes. That's exactly how your Account and Bank classes will behave.

What you'll build

The finished bank system can:

  • open accounts with automatic account numbers starting at 1001,
  • deposit and withdraw money, refusing zero, negative and too-large amounts,
  • transfer money between two accounts safely,
  • print a statement with every transaction and the balance after it,
  • list all accounts in number order.

Here's a quick taste (the user's typing follows each prompt):

=== Java Bank ===

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 1
Owner name: Sofia
Opened account 1001 for Sofia.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 2
Account number: 1001
Amount: $250
Deposited $250.00. New balance: $250.00

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 7
Thank you for banking with us. Goodbye!

Concepts used in this project

ConceptWhere it's usedLesson
Encapsulation: private fields, public behaviorAccountencapsulation, access modifiers
Method overloadingdeposit(cents) and deposit(cents, description)method overloading
RecordsTransactionrecords
Custom checked exceptionsInsufficientFundsExceptioncustom exceptions
TreeMap and OptionalStoring and finding accountsHashMap, Optional
Static helper methodsMoney.format(), Money.parse()static
Unmodifiable listsgetHistory()collections framework

How does a transfer work?

The bank keeps accounts in a map from account number to Account object. A transfer is simply a withdrawal from one account followed by a deposit into another. If the withdrawal fails, an exception stops the method before the deposit runs, so money can never appear out of nowhere.

The accounts TreeMap maps 1001 to Sofia's account and 1002 to James's account; bank.transfer(sofia, james, 15000) first withdraws from Sofia, changing her balance from $424.75 to $274.75 and adding a Transfer to 1002 line, then deposits into James's account, changing his balance from $80.00 to $230.00 and adding a Transfer from 1001 line
A transfer of $150: step 1 withdraws from account 1001, step 2 deposits into account 1002.

Step 1: Store money as cents

Before writing the account, decide how to store money. A double can't hold most decimal fractions exactly: 0.1 + 0.2 is 0.30000000000000004. Banks avoid this by counting whole cents in an integer type, so $424.75 is stored as 42475. A small Money helper class converts in both directions:

class Money {
    /** Turns 54525 cents into "$545.25". */
    static String format(long cents) {
        return String.format("$%d.%02d", cents / 100, cents % 100);
    }

    /** Turns text such as "12.5" into 1250 cents. */
    static long parse(String text) {
        double dollars = Double.parseDouble(text);
        if (Double.isNaN(dollars) || Double.isInfinite(dollars)) {
            throw new NumberFormatException("not a real amount");
        }
        return Math.round(dollars * 100);
    }
}

cents / 100 is integer division, giving the dollars, and cents % 100 gives the leftover cents. %02d pads the cents to two digits, so 5 cents prints as 05. Math.round() fixes tiny floating-point errors when converting the user's text, such as 0.29 * 100 being 28.999999999999996 (rounded to 29). parseDouble() also accepts the words NaN and Infinity, so we reject those explicitly.

Step 2: An encapsulated Account class

The Account class keeps its balance and history private. Code outside can read them through getters, but the only way to change the balance is deposit() or withdraw(), and both validate first. Each successful change adds a Transaction, a record that stores the description, the amount (negative for money going out) and the balance afterwards:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AccountTest {
    public static void main(String[] args) {
        Account account = new Account(1001, "Sofia");

        account.deposit(Money.parse("500"));
        try {
            account.withdraw(Money.parse("75.25"));
            account.withdraw(Money.parse("1000"));
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
        }
        try {
            account.deposit(Money.parse("-20"));
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
        account.deposit(Money.parse("120.50"));

        System.out.println();
        System.out.println("Statement for " + account.getNumber() + " (" + account.getOwner() + ")");
        for (Transaction t : account.getHistory()) {
            System.out.println(t.toLine());
        }
        System.out.println("Balance: " + Money.format(account.getBalanceCents()));
    }
}

class Account {
    private final int number;
    private final String owner;
    private long balanceCents;
    private final List<Transaction> history = new ArrayList<>();

    Account(int number, String owner) {
        this.number = number;
        this.owner = owner;
    }

    int getNumber() {
        return number;
    }

    String getOwner() {
        return owner;
    }

    long getBalanceCents() {
        return balanceCents;
    }

    List<Transaction> getHistory() {
        return Collections.unmodifiableList(history);
    }

    void deposit(long cents) {
        deposit(cents, "Deposit");
    }

    void deposit(long cents, String description) {
        checkPositive(cents);
        balanceCents += cents;
        history.add(new Transaction(description, cents, balanceCents));
    }

    void withdraw(long cents) throws InsufficientFundsException {
        withdraw(cents, "Withdrawal");
    }

    void withdraw(long cents, String description) throws InsufficientFundsException {
        checkPositive(cents);
        if (cents > balanceCents) {
            throw new InsufficientFundsException(balanceCents, cents);
        }
        balanceCents -= cents;
        history.add(new Transaction(description, -cents, balanceCents));
    }

    private static void checkPositive(long cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("The amount must be more than $0.00.");
        }
    }
}

record Transaction(String description, long amountCents, long balanceAfterCents) {
    String toLine() {
        String sign = amountCents >= 0 ? "+" : "-";
        return String.format("%-20s %11s %11s", description,
                sign + Money.format(Math.abs(amountCents)), Money.format(balanceAfterCents));
    }
}

class InsufficientFundsException extends Exception {
    private static final long serialVersionUID = 1L;

    InsufficientFundsException(long balanceCents, long requestedCents) {
        super("Insufficient funds: balance " + Money.format(balanceCents)
                + ", requested " + Money.format(requestedCents) + ".");
    }
}

class Money {
    /** Turns 54525 cents into "$545.25". */
    static String format(long cents) {
        return String.format("$%d.%02d", cents / 100, cents % 100);
    }

    /** Turns text such as "12.5" into 1250 cents. */
    static long parse(String text) {
        double dollars = Double.parseDouble(text);
        if (Double.isNaN(dollars) || Double.isInfinite(dollars)) {
            throw new NumberFormatException("not a real amount");
        }
        return Math.round(dollars * 100);
    }
}
Insufficient funds: balance $424.75, requested $1000.00.
The amount must be more than $0.00.

Statement for 1001 (Sofia)
Deposit                 +$500.00     $500.00
Withdrawal               -$75.25     $424.75
Deposit                 +$120.50     $545.25
Balance: $545.25

Let's trace it. After depositing $500 and withdrawing $75.25, the balance is $424.75, so the $1000 withdrawal throws InsufficientFundsException and the balance doesn't change. The -$20 deposit throws IllegalArgumentException from checkPositive(). Only the three successful operations appear in the statement.

Some details to notice:

  • Two kinds of exception. A negative amount is a mistake the caller should have prevented, so it's an unchecked IllegalArgumentException. Not having enough money is a normal business situation, so InsufficientFundsException is checked, and the compiler makes every caller of withdraw() deal with it.
  • Overloading. deposit(long cents) just calls deposit(cents, "Deposit"). The longer version lets transfers use their own descriptions later.
  • A read-only history. getHistory() returns Collections.unmodifiableList(history). Callers can loop over it, but calling add() on it throws an exception, so nobody can fake a transaction.
  • A fixed-format line. Transaction.toLine() uses %-20s and %11s to line up the columns, and puts + or - in front of the amount.

⚠️ Warning: no public setBalance()

It's tempting to add setBalance() "just in case". Don't. A public setter would let any code skip the validation and the history, which breaks the whole point of encapsulation. If a new operation is needed (like interest), add a method for it that follows the same rules.

Step 3: Many accounts in a Bank

The Bank class stores accounts in a TreeMap<Integer, Account>. A TreeMap works like a HashMap but keeps its keys sorted, so listing accounts always gives them in number order. find() returns an Optional<Account>, which says clearly "there may be no account with this number":

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

public class BankTest {
    public static void main(String[] args) {
        Bank bank = new Bank();
        Account sofia = bank.openAccount("Sofia");
        Account james = bank.openAccount("James");
        sofia.deposit(Money.parse("500"));
        james.deposit(Money.parse("80"));

        try {
            bank.transfer(sofia, james, Money.parse("150"));
            System.out.println("Transfer 1 done.");
            bank.transfer(james, sofia, Money.parse("900"));
            System.out.println("Transfer 2 done.");
        } catch (InsufficientFundsException e) {
            System.out.println("Transfer failed. " + e.getMessage());
        }

        for (Account account : bank.allAccounts()) {
            System.out.println(account.getNumber() + " " + account.getOwner() + ": "
                    + Money.format(account.getBalanceCents()));
        }

        System.out.println("Account 1005 exists? " + bank.find(1005).isPresent());
    }
}

class Bank {
    private final Map<Integer, Account> accounts = new TreeMap<>();
    private int nextNumber = 1001;

    Account openAccount(String owner) {
        Account account = new Account(nextNumber, owner);
        accounts.put(nextNumber, account);
        nextNumber++;
        return account;
    }

    Optional<Account> find(int number) {
        return Optional.ofNullable(accounts.get(number));
    }

    /** All accounts, in account-number order (a TreeMap keeps its keys sorted). */
    Collection<Account> allAccounts() {
        return accounts.values();
    }

    void transfer(Account from, Account to, long cents) throws InsufficientFundsException {
        if (from == to) {
            throw new IllegalArgumentException("Choose two different accounts.");
        }
        from.withdraw(cents, "Transfer to " + to.getNumber());
        to.deposit(cents, "Transfer from " + from.getNumber());
    }
}

class Account {
    private final int number;
    private final String owner;
    private long balanceCents;
    private final List<Transaction> history = new ArrayList<>();

    Account(int number, String owner) {
        this.number = number;
        this.owner = owner;
    }

    int getNumber() {
        return number;
    }

    String getOwner() {
        return owner;
    }

    long getBalanceCents() {
        return balanceCents;
    }

    List<Transaction> getHistory() {
        return Collections.unmodifiableList(history);
    }

    void deposit(long cents) {
        deposit(cents, "Deposit");
    }

    void deposit(long cents, String description) {
        checkPositive(cents);
        balanceCents += cents;
        history.add(new Transaction(description, cents, balanceCents));
    }

    void withdraw(long cents) throws InsufficientFundsException {
        withdraw(cents, "Withdrawal");
    }

    void withdraw(long cents, String description) throws InsufficientFundsException {
        checkPositive(cents);
        if (cents > balanceCents) {
            throw new InsufficientFundsException(balanceCents, cents);
        }
        balanceCents -= cents;
        history.add(new Transaction(description, -cents, balanceCents));
    }

    private static void checkPositive(long cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("The amount must be more than $0.00.");
        }
    }
}

record Transaction(String description, long amountCents, long balanceAfterCents) {
    String toLine() {
        String sign = amountCents >= 0 ? "+" : "-";
        return String.format("%-20s %11s %11s", description,
                sign + Money.format(Math.abs(amountCents)), Money.format(balanceAfterCents));
    }
}

class InsufficientFundsException extends Exception {
    private static final long serialVersionUID = 1L;

    InsufficientFundsException(long balanceCents, long requestedCents) {
        super("Insufficient funds: balance " + Money.format(balanceCents)
                + ", requested " + Money.format(requestedCents) + ".");
    }
}

class Money {
    /** Turns 54525 cents into "$545.25". */
    static String format(long cents) {
        return String.format("$%d.%02d", cents / 100, cents % 100);
    }

    /** Turns text such as "12.5" into 1250 cents. */
    static long parse(String text) {
        double dollars = Double.parseDouble(text);
        if (Double.isNaN(dollars) || Double.isInfinite(dollars)) {
            throw new NumberFormatException("not a real amount");
        }
        return Math.round(dollars * 100);
    }
}
Transfer 1 done.
Transfer failed. Insufficient funds: balance $230.00, requested $900.00.
1001 Sofia: $350.00
1002 James: $230.00
Account 1005 exists? false

The second transfer asks James for $900 when he only has $230. from.withdraw() throws, so the method stops right there and to.deposit() never runs. Sofia's balance is untouched. The if (from == to) check uses == on purpose: it asks "are these the very same object?", which is exactly the case we want to refuse.

Step 4: The menu

The BankApp menu has one neat trick: all error handling happens in one place. Helper methods such as askForAccount() and askForAmount() throw an IllegalArgumentException with a friendly message when something is wrong, and the loop in main() catches it:

try {
    switch (choice) {
        case "1" -> openAccount();
        case "2" -> deposit();
        // ...
        default -> System.out.println("Please choose a number from 1 to 7.");
    }
} catch (InsufficientFundsException | IllegalArgumentException e) {
    System.out.println("Error: " + e.getMessage());
} catch (InputEndedException e) {
    running = false;
}

Because of this, each menu action can be written as the "happy path" only. Look at transfer() in the full program: four lines with no if checks at all. Any problem jumps straight to the catch in main().

InputEndedException is a tiny unchecked exception of our own. It's thrown when hasNextLine() says the input has run out, so the loop can stop cleanly from deep inside any helper method.

To find an account, askForAccount() uses orElseThrow() on the Optional with a lambda that builds the exception: "give me the account, or throw this if there isn't one".

The complete bank account program in Java

Save this as BankApp.java and run it with java BankApp.java:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.TreeMap;

public class BankApp {
    static final Scanner input = new Scanner(System.in);
    static final Bank bank = new Bank();

    public static void main(String[] args) {
        System.out.println("=== Java Bank ===");
        boolean running = true;
        while (running) {
            System.out.println();
            System.out.println("1) Open account  2) Deposit  3) Withdraw  4) Transfer");
            System.out.println("5) Statement  6) All accounts  7) Quit");
            String choice = readLine("Choose an option: ");
            if (choice == null) {
                break;
            }
            try {
                switch (choice) {
                    case "1" -> openAccount();
                    case "2" -> deposit();
                    case "3" -> withdraw();
                    case "4" -> transfer();
                    case "5" -> printStatement();
                    case "6" -> listAccounts();
                    case "7" -> running = false;
                    default -> System.out.println("Please choose a number from 1 to 7.");
                }
            } catch (InsufficientFundsException | IllegalArgumentException e) {
                System.out.println("Error: " + e.getMessage());
            } catch (InputEndedException e) {
                running = false;
            }
        }
        System.out.println("Thank you for banking with us. Goodbye!");
    }

    static void openAccount() {
        String owner = readLine("Owner name: ");
        if (owner == null) {
            throw new InputEndedException();
        }
        if (owner.isEmpty()) {
            throw new IllegalArgumentException("The name can't be empty.");
        }
        Account account = bank.openAccount(owner);
        System.out.println("Opened account " + account.getNumber() + " for " + owner + ".");
    }

    static void deposit() {
        Account account = askForAccount("Account number: ");
        long cents = askForAmount();
        account.deposit(cents);
        System.out.println("Deposited " + Money.format(cents) + ". New balance: "
                + Money.format(account.getBalanceCents()));
    }

    static void withdraw() throws InsufficientFundsException {
        Account account = askForAccount("Account number: ");
        long cents = askForAmount();
        account.withdraw(cents);
        System.out.println("Withdrew " + Money.format(cents) + ". New balance: "
                + Money.format(account.getBalanceCents()));
    }

    static void transfer() throws InsufficientFundsException {
        Account from = askForAccount("From account: ");
        Account to = askForAccount("To account: ");
        long cents = askForAmount();
        bank.transfer(from, to, cents);
        System.out.println("Transferred " + Money.format(cents) + " from " + from.getNumber()
                + " to " + to.getNumber() + ".");
    }

    static void printStatement() {
        Account account = askForAccount("Account number: ");
        System.out.println("Statement for " + account.getNumber() + " (" + account.getOwner() + ")");
        if (account.getHistory().isEmpty()) {
            System.out.println("No transactions yet.");
        }
        for (Transaction t : account.getHistory()) {
            System.out.println(t.toLine());
        }
        System.out.println("Balance: " + Money.format(account.getBalanceCents()));
    }

    static void listAccounts() {
        if (bank.allAccounts().isEmpty()) {
            System.out.println("No accounts yet.");
        }
        for (Account account : bank.allAccounts()) {
            System.out.println(account.getNumber() + "  " + account.getOwner() + "  "
                    + Money.format(account.getBalanceCents()));
        }
    }

    /** Reads an account number and finds the account, or throws IllegalArgumentException. */
    static Account askForAccount(String prompt) {
        String text = readLine(prompt);
        if (text == null) {
            throw new InputEndedException();
        }
        int number;
        try {
            number = Integer.parseInt(text);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Account numbers are whole numbers like 1001.");
        }
        return bank.find(number)
                .orElseThrow(() -> new IllegalArgumentException("No account with number " + number + "."));
    }

    /** Reads an amount in dollars such as 12.50 and returns it in cents. */
    static long askForAmount() {
        String text = readLine("Amount: $");
        if (text == null) {
            throw new InputEndedException();
        }
        try {
            return Money.parse(text);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("\"" + text + "\" is not an amount.");
        }
    }

    /** Prints the prompt and reads one trimmed line. Returns null when input ends. */
    static String readLine(String prompt) {
        System.out.print(prompt);
        if (!input.hasNextLine()) {
            System.out.println();
            return null;
        }
        return input.nextLine().trim();
    }
}

/** Thrown when there is no more input to read, so the menu loop can stop. */
class InputEndedException extends RuntimeException {
    private static final long serialVersionUID = 1L;
}

class Bank {
    private final Map<Integer, Account> accounts = new TreeMap<>();
    private int nextNumber = 1001;

    Account openAccount(String owner) {
        Account account = new Account(nextNumber, owner);
        accounts.put(nextNumber, account);
        nextNumber++;
        return account;
    }

    Optional<Account> find(int number) {
        return Optional.ofNullable(accounts.get(number));
    }

    /** All accounts, in account-number order (a TreeMap keeps its keys sorted). */
    Collection<Account> allAccounts() {
        return accounts.values();
    }

    void transfer(Account from, Account to, long cents) throws InsufficientFundsException {
        if (from == to) {
            throw new IllegalArgumentException("Choose two different accounts.");
        }
        from.withdraw(cents, "Transfer to " + to.getNumber());
        to.deposit(cents, "Transfer from " + from.getNumber());
    }
}

class Account {
    private final int number;
    private final String owner;
    private long balanceCents;
    private final List<Transaction> history = new ArrayList<>();

    Account(int number, String owner) {
        this.number = number;
        this.owner = owner;
    }

    int getNumber() {
        return number;
    }

    String getOwner() {
        return owner;
    }

    long getBalanceCents() {
        return balanceCents;
    }

    List<Transaction> getHistory() {
        return Collections.unmodifiableList(history);
    }

    void deposit(long cents) {
        deposit(cents, "Deposit");
    }

    void deposit(long cents, String description) {
        checkPositive(cents);
        balanceCents += cents;
        history.add(new Transaction(description, cents, balanceCents));
    }

    void withdraw(long cents) throws InsufficientFundsException {
        withdraw(cents, "Withdrawal");
    }

    void withdraw(long cents, String description) throws InsufficientFundsException {
        checkPositive(cents);
        if (cents > balanceCents) {
            throw new InsufficientFundsException(balanceCents, cents);
        }
        balanceCents -= cents;
        history.add(new Transaction(description, -cents, balanceCents));
    }

    private static void checkPositive(long cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("The amount must be more than $0.00.");
        }
    }
}

record Transaction(String description, long amountCents, long balanceAfterCents) {
    String toLine() {
        String sign = amountCents >= 0 ? "+" : "-";
        return String.format("%-20s %11s %11s", description,
                sign + Money.format(Math.abs(amountCents)), Money.format(balanceAfterCents));
    }
}

class InsufficientFundsException extends Exception {
    private static final long serialVersionUID = 1L;

    InsufficientFundsException(long balanceCents, long requestedCents) {
        super("Insufficient funds: balance " + Money.format(balanceCents)
                + ", requested " + Money.format(requestedCents) + ".");
    }
}

class Money {
    /** Turns 54525 cents into "$545.25". */
    static String format(long cents) {
        return String.format("$%d.%02d", cents / 100, cents % 100);
    }

    /** Turns text such as "12.5" into 1250 cents. */
    static long parse(String text) {
        double dollars = Double.parseDouble(text);
        if (Double.isNaN(dollars) || Double.isInfinite(dollars)) {
            throw new NumberFormatException("not a real amount");
        }
        return Math.round(dollars * 100);
    }
}

Sample run

Here's a sample run (the user's typing follows each prompt). It opens two accounts, tries a withdrawal that's too large, transfers $150, then tries a negative deposit, a missing account and a transfer to the same account before printing Sofia's statement:

=== Java Bank ===

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 1
Owner name: Sofia
Opened account 1001 for Sofia.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 1
Owner name: James
Opened account 1002 for James.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 2
Account number: 1001
Amount: $500
Deposited $500.00. New balance: $500.00

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 2
Account number: 1002
Amount: $80
Deposited $80.00. New balance: $80.00

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 3
Account number: 1002
Amount: $120
Error: Insufficient funds: balance $80.00, requested $120.00.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 3
Account number: 1001
Amount: $75.25
Withdrew $75.25. New balance: $424.75

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 4
From account: 1001
To account: 1002
Amount: $150
Transferred $150.00 from 1001 to 1002.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 2
Account number: 1001
Amount: $-20
Error: The amount must be more than $0.00.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 2
Account number: 1009
Error: No account with number 1009.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 4
From account: 1001
To account: 1001
Amount: $10
Error: Choose two different accounts.

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 5
Account number: 1001
Statement for 1001 (Sofia)
Deposit                 +$500.00     $500.00
Withdrawal               -$75.25     $424.75
Transfer to 1002        -$150.00     $274.75
Balance: $274.75

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 6
1001  Sofia  $274.75
1002  James  $230.00

1) Open account  2) Deposit  3) Withdraw  4) Transfer
5) Statement  6) All accounts  7) Quit
Choose an option: 7
Thank you for banking with us. Goodbye!

Every refused action left the balances untouched, and only real changes made it into the statement.

Common mistakes in this project

1. Making the balance public

A public double balance field lets any code write account.balance = -500. Keep fields private and expose behavior (deposit(), withdraw()) instead of raw data.

2. Changing the balance before checking

balanceCents -= cents;          // too early!
if (balanceCents < 0) {
    throw new InsufficientFundsException(...);  // the balance is already wrong
}

Check first, change second. Then a thrown exception always leaves the account exactly as it was.

3. Depositing before withdrawing in a transfer

If transfer() calls to.deposit() first and from.withdraw() then fails, the bank has created money from nothing. Always do the step that can fail first.

4. Using double for money

Adding 10 cents ten times as double gives 0.9999999999999999, not 1.0. Count cents in a long, as this project does, or use java.math.BigDecimal in real financial software.

Level up: challenges

🏋️ Challenges

  1. Overdraft limit. Let each account go down to -$100.00. Store the limit as a field and change the check in withdraw(). (Watch out: Money.format() needs updating for negative balances.)
  2. Close an account. Add a menu option that removes an account from the map, but only if its balance is exactly zero.
  3. Interest. Add addInterest(double ratePercent) that deposits the interest, rounded to whole cents, with the description "Interest".
  4. Daily withdrawal limit. Refuse withdrawals once an account has taken out more than $1,000.00 in total during the session. Create a LimitExceededException.
  5. Search by owner. List every account whose owner name contains some text, ignoring case.
Show a hint for challenge 1
private final long overdraftCents = 10000;

// in withdraw(), replace the balance check with:
if (balanceCents - cents < -overdraftCents) {
    throw new InsufficientFundsException(balanceCents, cents);
}

// in Money.format(), handle the sign first:
String sign = cents < 0 ? "-" : "";
long abs = Math.abs(cents);
return String.format("%s$%d.%02d", sign, abs / 100, abs % 100);
Show a hint for challenge 3

Calculate long interest = Math.round(balanceCents * ratePercent / 100);, and only call deposit(interest, "Interest") if interest > 0, because deposit() refuses zero.

What's next?

You've built a system that protects its data with encapsulation and exceptions. To finish the projects chapter, you'll build a two-player game on a 2D array board: tic-tac-toe in Java. Want to compare with a language without classes? See the bank account project in C.

Key takeaways

  • A bank account program in Java keeps the balance private and changes it only through deposit() and withdraw() methods that validate every amount.
  • Store money as whole cents in a long to avoid floating-point rounding errors, and format it as dollars only for display.
  • Throw an unchecked IllegalArgumentException for invalid amounts and a checked InsufficientFundsException when the balance is too low.
  • Record each successful change as a Transaction record, and return the history as an unmodifiable list.
  • A transfer withdraws first and deposits second, so a failed withdrawal stops the method before any money moves.
  • A TreeMap keeps accounts sorted by number, and Optional makes "no such account" explicit.

Frequently asked questions

How do you create a bank account program in Java?

Write an Account class with private fields for the number, owner and balance, plus deposit() and withdraw() methods that validate amounts before changing the balance. Store several accounts in a map by account number and drive everything with a Scanner menu loop.

How do you prevent withdrawing more than the balance in Java?

In withdraw(), compare the requested amount with the balance before changing anything. If the amount is larger, throw a custom InsufficientFundsException so the caller must handle it, and leave the balance unchanged.

Should InsufficientFundsException be checked or unchecked?

Checked is a good choice, because running out of money is an expected situation that every caller should handle. Programming mistakes, such as passing a negative amount, are better as unchecked exceptions like IllegalArgumentException.

Why should I not use double for money in Java?

double stores numbers in binary and cannot represent most decimal fractions exactly, so sums like 0.1 + 0.2 give 0.30000000000000004. Use a long number of cents or java.math.BigDecimal to keep money exact.

How do you keep a transaction history for an account?

Give the account a private List of Transaction objects and add one inside deposit() and withdraw() after each successful change. A record such as Transaction(description, amountCents, balanceAfterCents) is a compact way to store each entry.