Build a Bank Account Simulator with Java

Build a Java terminal app that safely records bank account transactions.

Introduction

30 Second Summary

Every deposit or withdrawal should leave a bank balance trustworthy. A missing rule can turn one ordinary transaction into a negative balance.

In this project, you will build a terminal-based bank account simulator with Java. Your simulator will reject an overdraft without changing its valid balance or transaction history.

What You'll Build

When you run your finished simulator, the terminal prints a rejected transaction followed by an account statement whose balance remains safe.

By the end of this project, you'll have:

  • A readable account statement that lists successful transactions before showing the final rupiah balance.
  • An overdraft safeguard that stops an unsafe withdrawal from changing the account state.
  • A public GitHub repository where reviewers can follow README.md to run your simulator.
  • Secret Mission: Add a safe account transfer that validates the amount before recording matching entries for both accounts.

Do I need Java experience?

Basic familiarity with object-oriented programming is enough. You do not need Java experience because the project includes the Oracle JDK 25 setup.

Before We Start

This step gives your bank account simulator a clear purpose before the hands-on work begins. Its object-oriented model protects balances through recorded transactions plus business-rule validation.

Set Up Java on Your Mac

The bank simulator cannot run until your Mac has a verified Oracle JDK 25. This step removes the only unconfirmed dependency before you write Java code.

Visual Studio Code gives you an integrated terminal for running the simulator. You will finish with an empty bank-account-simulator workspace that is ready for your first Java class.

In this step, get ready to:
  • Check whether your Mac already runs Oracle JDK 25.
  • Install the correct JDK 25 package when Java is missing or outdated.
  • Prepare an empty bank-account-simulator workspace with Java 25 verified inside it.
Check your current Java version

The Java launcher can report the default JDK available to your Mac. This first check tells you whether installation is necessary.

  • Press Cmd+Space to open macOS search.
  • Type Visual Studio Code into the search bar.
  • Press Return to launch Visual Studio Code.
  • Select View from the menu bar.
  • Select Terminal to open the integrated terminal.
  • Check the installed Java version by running this command:
java -version

What does this command check?

  • The java command starts the Java launcher available to your terminal.
  • The -version option prints the default installed JDK version.
  • An output containing 25 confirms that your Mac has the required JDK.
Install the required JDK if needed

Match your terminal result to one of the paths below. Each path ends with the same goal: Oracle JDK 25 available to Visual Studio Code.

✔️ I see version 25

Good news. Your Mac already reports version 25. The required JDK is ready for the workspace check.

ⓧ I see an older version

Your Java launcher works with an older JDK. Installing Oracle JDK 25 gives this project the required version.

  • Open the official Oracle Java Downloads page.
  • Locate the macOS downloads for Java SE Development Kit 25.0.4.1.
  • Download jdk-25_macos-aarch64_bin.dmg if your Mac uses Apple Silicon.
  • Download jdk-25_macos-x64_bin.dmg if your Mac uses an Intel processor.

The downloaded DMG contains the macOS installer for your processor type.

  • Open the downloaded DMG.
  • Open JDK 25.pkg from the installer window.
  • Select Continue.
  • Select Install.
  • Select Install Software to complete the installation.

The installer adds Oracle JDK 25 to your Mac. The workspace check below confirms that Visual Studio Code can use it.

ⓧ Command not found

The missing command means your terminal cannot find a JDK. Oracle provides separate JDK 25 installers for Apple Silicon and Intel Macs.

  • Open the official Oracle Java Downloads page.
  • Locate the macOS downloads for Java SE Development Kit 25.0.4.1.
  • Download jdk-25_macos-aarch64_bin.dmg if your Mac uses Apple Silicon.
  • Download jdk-25_macos-x64_bin.dmg if your Mac uses an Intel processor.

The downloaded DMG contains the macOS installer for your processor type.

  • Open the downloaded DMG.
  • Open JDK 25.pkg from the installer window.
  • Select Continue.
  • Select Install.
  • Select Install Software to complete the installation.

That completes the JDK installation. The final check below confirms that the Java launcher is now available.

Installer not completing?

  • Confirm that the downloaded filename matches your Mac processor.
  • Open the DMG again if the package window was closed.
  • Restart the installer from JDK 25.pkg.

Get targeted help with your installation: Help me install Oracle JDK 25 on my Mac.

Prepare your Java workspace

A Visual Studio Code workspace keeps the integrated terminal anchored to your project folder. Creating it on the Desktop also makes the folder easy to find throughout the project.

  • Click a blank area of your Desktop.
  • Select File from the macOS menu bar.
  • Select New Folder.
  • Type bank-account-simulator as the folder name.
  • Press Return to save the name.

That is your clean starting point. The bank-account-simulator folder is empty and ready to become a Java project.

  • Switch back to Visual Studio Code from earlier.
  • Select File from the menu bar.
  • Select Open Folder....
  • Navigate to the Desktop in the folder picker.
  • Select the bank-account-simulator folder.
  • Select Open to use the folder as your workspace.

The Explorer sidebar now shows bank-account-simulator with no source files inside it. This confirms that you opened the correct empty folder.

  • Select View from the menu bar.
  • Select Terminal to open a terminal inside the workspace.

Before you run this final check, do you expect the workspace terminal to report Java version 25?

  • Verify Java from the workspace terminal by running this command:
java -version

What does this result prove?

  • An output containing version 25 confirms that the required JDK is active.
  • Running the check in the integrated terminal confirms that Visual Studio Code can access Java.
  • The terminal starts inside the open workspace folder. Your future Java commands run against this project.

You have cleared the setup hurdle. Your empty workspace can now run Java 25 from inside Visual Studio Code.

Still seeing an older version?

  • Close every Visual Studio Code window.
  • Reopen the bank-account-simulator workspace.
  • Repeat the version check in a new integrated terminal.

Get help if the old version remains active: Help me make Oracle JDK 25 the active Java version on my Mac.

Keep Learning with Dev.java

The official Dev.java Learn Java portal organizes lessons on getting started with Java. It also covers language basics, collections, and exceptions.

Your Mac now has the JDK and workspace needed for the simulator. Next up, you will create an account object and print its first deposited balance.

Build Your First Bank Account

Your Oracle JDK 25 setup is ready. The empty bank-account-simulator workspace is open in Visual Studio Code.

Now you'll build the smallest useful Java banking result. One account accepts a deposit. Its balance appears in the terminal before transaction history or safeguards add more complexity.

In this step, get ready to:
  • Model a bank account with private identity fields and a private balance.
  • Create a runnable Java entry point that deposits 500000.
  • Run the simulator and confirm that the terminal prints the deposited balance.
Create the account model

A class keeps related data with the methods that manage it. Encapsulation keeps each account's identity and balance inside Account.

  • Select the bank-account-simulator folder in the Explorer sidebar.
  • Click the New File icon at the top of the Explorer sidebar.
  • Type Account.java into the file name field.
  • Press Enter.
  • Add the account model to Account.java by pasting this code:
public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;

    public Account(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
    }

    public void deposit(long amount) {
        balance += amount;
    }

    public long getBalance() {
        return balance;
    }
}

What Does This Code Do?

  • The accountNumber field stores the account's identifier.
  • The ownerName field stores the account owner's name.
  • The balance field stores a whole rupiah amount as a long value.
  • The Account constructor sets the identity fields when you create an account.
  • The deposit(long amount) method increases the private balance.
  • The getBalance() method provides controlled access to the current balance.
  • Save Account.java.
  • Confirm that Account.java appears under bank-account-simulator in the Explorer sidebar.

Can't Find Account.java?

Check that Account.java sits directly inside the bank-account-simulator folder. Confirm that its name uses a capital A because Java class names and file names must match.

Help me check my Account.java file location and class name.

Create and run the first deposit

The main method is the program's starting point. It creates account NW-1001 before depositing 500000 rupiah.

  • Select the bank-account-simulator folder in the Explorer sidebar.
  • Click the New File icon at the top of the Explorer sidebar.
  • Type Main.java into the file name field.
  • Press Enter.
  • Add the runnable deposit to Main.java by pasting this code:
public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);

        System.out.println("Saldo: Rp" + account.getBalance());
    }
}

How Does the Deposit Run?

  • The public static void main(String[] args) method gives Java an entry point for the program.
  • The new Account("NW-1001", "Nama Anda") expression creates the account object.
  • The account.deposit(500000) call increases its balance.
  • The System.out.println call prints the balance returned by getBalance().
  • Save Main.java.
  • Switch back to the integrated terminal from earlier.

Before you run the program, what balance do you expect after depositing 500000 into an empty account?

  • Run the simulator from the bank-account-simulator folder by running this command:
java Main.java

What Does This Command Do?

The java Main.java command launches Main.java in source-file mode. Java resolves the required Account.java class from the same folder.

You'll see Saldo: Rp500000 in the terminal. This confirms that the account accepted the deposit before returning its balance.

You have your first banking result working: the terminal reports the account's Rp500000 balance.

Program Not Showing the Balance?

Make sure both Java files are saved directly inside bank-account-simulator. Check that the class names match Account.java and Main.java exactly.

Help me troubleshoot why java Main.java does not print Saldo: Rp500000.

✔️ Awesome, I've got everything!

Great. Double check that both Java files are saved. Confirm that the terminal prints Saldo: Rp500000.

ⓧ I'd like to double check the full code

Compare your two files with the complete code below:

public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;

    public Account(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
    }

    public void deposit(long amount) {
        balance += amount;
    }

    public long getBalance() {
        return balance;
    }
}
public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);

        System.out.println("Saldo: Rp" + account.getBalance());
    }
}

Your account now accepts deposits. Next, you'll record transactions before testing what happens when a withdrawal exceeds the balance.

Expose the Negative-Balance Bug

Your first account can accept a deposit and show its balance. However, encapsulation only protects the fields from direct outside access.

The methods inside Account still trust every amount they receive. You will add transaction history before deliberately withdrawing more money than the account holds.

In this step, get ready to:
  • Model deposits and withdrawals as transaction types.
  • Record each balance change in an ArrayList.
  • Run an overdraft that exposes the negative-balance bug.
Create the transaction model

An enum gives each transaction one known type. A transaction object pairs that type with the amount shown in the account statement.

  • In the Explorer sidebar of the Visual Studio Code workspace from earlier, click the New File icon.
  • Enter TransactionType.java as the file name.
  • Define the available transaction types by pasting this code into TransactionType.java:
public enum TransactionType {
    DEPOSIT,
    WITHDRAWAL
}

What Does This Enum Do?

  • TransactionType limits each transaction to a defined category.
  • DEPOSIT represents money entering the account.
  • WITHDRAWAL represents money leaving the account.
  • Save TransactionType.java.
  • Confirm that TransactionType.java appears in the Explorer sidebar.

Can't See the Enum File?

Check that the file name ends with .java. Make sure the file sits directly inside bank-account-simulator.

Help me check my transaction type file.

  • Click the New File icon in the Explorer sidebar.
  • Enter Transaction.java as the file name.
  • Define the transaction details by pasting this code into Transaction.java:
public class Transaction {
    private final TransactionType type;
    private final long amount;

    public Transaction(TransactionType type, long amount) {
        this.type = type;
        this.amount = amount;
    }

    public String toDisplayText() {
        return type + " Rp" + amount;
    }
}

What Does This Class Do?

  • type stores whether the transaction is a deposit or withdrawal.
  • amount stores the whole rupiah value as a long.
  • toDisplayText() builds the text that appears in the account statement.
  • Save Transaction.java.
  • Confirm that Transaction.java appears beside TransactionType.java in the Explorer sidebar.

Can't See Both Transaction Files?

Check that both file names use the same capitalization as their class names. Confirm that neither file was created inside another folder.

Help me check my Java transaction files.

Record account transactions

An ArrayList keeps transaction objects in the order they were added. The account can use that ordered history to explain how its balance changed.

  • Return to Account.java from earlier.
  • Find this opening code:
public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;

What Are You Locating?

This opening defines the account class and its current fields. The account has no collection for remembering transactions yet.

  • Replace that opening code with this version:
import java.util.ArrayList;

public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;
    private final ArrayList<Transaction> transactions = new ArrayList<>();

How Does the History Work?

  • java.util.ArrayList makes the collection type available to the account class.
  • transactions starts as an empty list for this account's history.
  • final keeps the field connected to the same list throughout the account's lifetime.
  • Save Account.java.
  • Return to the integrated terminal from earlier.
  • Confirm that the new transaction collection compiles by running this command:
java Main.java

You'll see Saldo: Rp500000 again. This confirms that the account can create its empty transaction history without breaking the working deposit.

Seeing a Compilation Error?

Check that Transaction.java and TransactionType.java are saved beside Account.java. Compare the capitalization of every class name.

Help me fix the transaction collection error.

  • Inside deposit(long amount), find this balance update:
balance += amount;

What Does the Deposit Do Now?

The current deposit only increases the balance. It leaves no record that explains where the money came from.

  • Replace that balance update with these lines:
balance += amount;
transactions.add(new Transaction(TransactionType.DEPOSIT, amount));

How Is the Deposit Recorded?

The first line updates the balance. The second line appends a matching DEPOSIT transaction to the history.

  • Save Account.java.
  • Confirm that the recorded deposit still runs by using this command:
java Main.java

You'll still see Saldo: Rp500000. The same deposit now changes the balance and creates a history entry.

Does the Deposit Fail to Compile?

Check that TransactionType.DEPOSIT matches the enum value exactly. Make sure the new transaction line stays inside deposit(long amount).

Help me fix the recorded deposit.

  • In Account.java, find the closing brace of deposit(long amount).
  • Add this withdrawal method immediately below it:
public void withdraw(long amount) {
    balance -= amount;
    transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
}

How Does Withdrawal Work?

  • balance -= amount subtracts the requested amount from the account.
  • TransactionType.WITHDRAWAL records the matching withdrawal in the history.
  • The method currently accepts every amount it receives.
  • Save Account.java.
  • Confirm that the new withdrawal method compiles by running this command:
java Main.java

You'll see Saldo: Rp500000 once more. The withdrawal method is available even though Main.java does not call it yet.

Does the Withdrawal Fail to Compile?

Check that withdraw(long amount) sits inside the Account class. Confirm that WITHDRAWAL matches the enum value.

Help me fix the withdrawal method.

Trigger the negative balance

A readable statement makes the account's internal state visible. It shows every recorded transaction before printing the final balance.

  • In Account.java, find getBalance().
  • Add this statement method immediately below it:
public void printStatement() {
    System.out.println("Mutasi rekening " + accountNumber + " - " + ownerName);

    for (Transaction transaction : transactions) {
        System.out.println(transaction.toDisplayText());
    }

    System.out.println("Saldo akhir: Rp" + balance);
}

How Is the Statement Printed?

  • The first line identifies the account number and owner.
  • The loop prints each transaction through toDisplayText().
  • The final line reveals the balance left after every recorded transaction.
  • Save Account.java.
  • Confirm that the statement method compiles by running this command:
java Main.java

You'll see the existing Saldo: Rp500000 output. This confirms that the statement method compiles before you call it.

Does the Statement Fail to Compile?

Check that the loop uses Transaction as its item type. Confirm that toDisplayText() uses the same capitalization as the method in Transaction.java.

Help me fix the account statement.

  • Return to Main.java from earlier.
  • Find the line System.out.println("Saldo: Rp" + account.getBalance());.
  • Replace the deposit and balance-printing lines with this code:
account.deposit(500000);
account.withdraw(700000);

account.printStatement();

What Will Main Test?

  • deposit(500000) gives the account its starting balance.
  • withdraw(700000) requests more money than the account holds.
  • printStatement() exposes the resulting history and balance.
  • Save Main.java.

Before you run the program, do you think the account will refuse the withdrawal or accept it?

  • Expose the current withdrawal behavior by running this command:
java Main.java

You'll see WITHDRAWAL Rp700000 in the statement. You'll also see Saldo akhir: Rp-200000 at the bottom.

That negative result is the intended failure for this step. The account recorded an overdraft because its withdrawal method has no business rule protecting the balance.

Don't See the Negative Balance?

Confirm that Main.java calls account.withdraw(700000) before account.printStatement(). Check that withdraw(long amount) subtracts the amount before recording the transaction.

Help me reproduce the negative-balance bug.

✔️ Awesome, I've got everything!

Great work. Your simulator now records transactions and exposes the exact overdraft bug that its business rules need to prevent.

ⓧ I'd like to double check the full code

Compare each saved file with the complete versions below.

import java.util.ArrayList;

public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;
    private final ArrayList<Transaction> transactions = new ArrayList<>();

    public Account(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
    }

    public void deposit(long amount) {
        balance += amount;
        transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
    }

    public void withdraw(long amount) {
        balance -= amount;
        transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
    }

    public long getBalance() {
        return balance;
    }

    public void printStatement() {
        System.out.println("Mutasi rekening " + accountNumber + " - " + ownerName);

        for (Transaction transaction : transactions) {
            System.out.println(transaction.toDisplayText());
        }

        System.out.println("Saldo akhir: Rp" + balance);
    }
}

Account.java Checklist

This file stores the balance and transaction history. Its current withdrawal method deliberately permits the negative result you observed.

public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);
        account.withdraw(700000);

        account.printStatement();
    }
}

Main.java Checklist

This file creates account NW-1001. It runs the deposit and oversized withdrawal before printing the statement.

public class Transaction {
    private final TransactionType type;
    private final long amount;

    public Transaction(TransactionType type, long amount) {
        this.type = type;
        this.amount = amount;
    }

    public String toDisplayText() {
        return type + " Rp" + amount;
    }
}

Transaction.java Checklist

This file pairs each transaction type with an amount. It also provides the display text used by the statement.

public enum TransactionType {
    DEPOSIT,
    WITHDRAWAL
}

TransactionType.java Checklist

This file defines the two transaction categories used by the account history.

You have proved why private fields alone cannot enforce banking rules. Next, you will reject unsafe transactions before they can change the balance or history.

Enforce Safe Transaction Rules

Your last run exposed the cost of an account model without an overdraft rule. The withdrawal changed the balance to a negative value.

In Java, encapsulation keeps the balance behind Account methods. This step adds a custom unchecked exception that rejects invalid transactions before they change the account.

In this step, get ready to:
  • Create a custom exception for rejected transactions.
  • Validate every requested amount before changing account state.
  • Confirm that a rejected overdraft preserves the account state.
Create the rejection exception

A dedicated exception gives the account one clear way to report a broken business rule. Its message explains why the transaction was rejected.

  • Right-click the bank-account-simulator folder in the VS Code file sidebar.
  • Select the option for creating a new file.
  • Name the file TransactionRejectedException.java.
  • Define the exception by pasting this code into TransactionRejectedException.java:
public class TransactionRejectedException extends RuntimeException {
    public TransactionRejectedException(String message) {
        super(message);
    }
}

What Does This Exception Do?

TransactionRejectedException extends RuntimeException, which makes it unchecked. Account methods can throw it as soon as a transaction breaks a rule.

The constructor passes the rejection reason to the parent class. The program can retrieve that reason later with getMessage().

  • Save TransactionRejectedException.java.
  • Confirm that TransactionRejectedException.java appears beside your other Java files.

Seeing an Exception Class Error?

Check that the filename is exactly TransactionRejectedException.java. A public Java class must match its filename.

Compare the class name everywhere if the editor still highlights it. Help me fix my TransactionRejectedException class.

Protect the account before mutation

The account owns its balance and transaction history. Every validation check must run before balance or transactions changes.

  • In Account.java, place this helper immediately before the final closing brace:
    private void validatePositiveAmount(long amount) {
        if (amount <= 0) {
            throw new TransactionRejectedException("Nominal harus lebih besar dari nol");
        }
    }

What Does This Helper Do?

validatePositiveAmount() rejects zero or negative amounts through one reusable rule. This keeps the same requirement available to every transaction method.

  • In Account.java, replace the existing withdraw(long amount) method with this version:
    public void withdraw(long amount) {
        validatePositiveAmount(amount);

        if (amount > balance) {
            throw new TransactionRejectedException(
                    "Saldo Rp" + balance + " tidak cukup untuk menarik Rp" + amount
            );
        }

        balance -= amount;
        transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
    }

How Is the Balance Protected?

The method validates the amount first. It then compares the requested withdrawal with balance.

An overdraft throws TransactionRejectedException before the subtraction or history update. Rejected transactions leave the account state untouched.

  • Save Account.java.

Before you run the unchanged scenario, do you expect the account to print a negative statement or stop the withdrawal?

  • Test the unchanged overdraft scenario by running this command in the integrated terminal from earlier:
java Main.java

This Stop Is Expected

A red stack trace can look alarming. Here it proves that the account rejected the old withdrawal before printing a negative statement.

The program stops because Main.java does not catch the new exception yet. You will add that handling in the final substep.

Still Seeing a Negative Balance?

Check that the balance comparison appears before balance -= amount;. Also confirm that your withdraw(long amount) method calls validatePositiveAmount(amount).

Help me find why my withdrawal still creates a negative balance.

  • In Account.java, replace the existing deposit(long amount) method with this version:
    public void deposit(long amount) {
        validatePositiveAmount(amount);
        balance += amount;
        transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
    }

Why Validate Deposits Too?

A deposit must represent a positive amount. Calling validatePositiveAmount(amount) before the addition prevents zero or negative deposits from changing the account.

  • Save Account.java.
  • Recheck the account rules by running this command:
java Main.java

What Did This Rerun Confirm?

The positive deposit succeeds before the old overdraft request reaches the new withdrawal rule. The custom exception then stops the invalid withdrawal.

Catch the overdraft and verify state

A rejected transaction is an expected business outcome. A try and catch block lets the terminal report that outcome while the program continues to print the preserved statement.

  • In Main.java, replace the entire class with this version:
public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);
        account.withdraw(200000);

        try {
            account.withdraw(400000);
        } catch (TransactionRejectedException exception) {
            System.out.println("Transaksi ditolak: " + exception.getMessage());
        }

        System.out.println();
        account.printStatement();
    }
}

What Does the Final Flow Do?

The account accepts the deposit and the first withdrawal. Its balance becomes Rp300000.

The next withdrawal requests more than that balance. The catch block prints the exception message before the statement displays only successful transactions.

  • Save Main.java.

Before you run the final program, where do you expect the balance to stop after the valid withdrawal?

  • Verify the safe transaction flow by running this command:
java Main.java

What Should You See?

The terminal should show the rejection reason before the account statement. Check for these exact lines:

  • Transaksi ditolak: Saldo Rp300000 tidak cukup untuk menarik Rp400000.
  • DEPOSIT Rp500000.
  • WITHDRAWAL Rp200000.
  • Saldo akhir: Rp300000.

That safeguard is doing its job. The rejected withdrawal leaves both the valid balance and transaction history untouched.

Not Seeing the Preserved Balance?

Confirm that Main.java withdraws 200000 before attempting 400000. Make sure the overdraft call sits inside the try block.

Check that Account.java throws the exception before changing balance or adding a withdrawal transaction. Help me debug my preserved balance and transaction history.

✔️ Awesome, I've got everything!

Great work. Save every Java file and confirm that the terminal ends with Saldo akhir: Rp300000.

ⓧ I'd like to double check the full code

Compare each file with the complete safe transaction version below.

import java.util.ArrayList;

public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;
    private final ArrayList<Transaction> transactions = new ArrayList<>();

    public Account(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
    }

    public void deposit(long amount) {
        validatePositiveAmount(amount);
        balance += amount;
        transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
    }

    public void withdraw(long amount) {
        validatePositiveAmount(amount);

        if (amount > balance) {
            throw new TransactionRejectedException(
                    "Saldo Rp" + balance + " tidak cukup untuk menarik Rp" + amount
            );
        }

        balance -= amount;
        transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
    }

    public long getBalance() {
        return balance;
    }

    public void printStatement() {
        System.out.println("Mutasi rekening " + accountNumber + " - " + ownerName);

        for (Transaction transaction : transactions) {
            System.out.println(transaction.toDisplayText());
        }

        System.out.println("Saldo akhir: Rp" + balance);
    }

    private void validatePositiveAmount(long amount) {
        if (amount <= 0) {
            throw new TransactionRejectedException("Nominal harus lebih besar dari nol");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);
        account.withdraw(200000);

        try {
            account.withdraw(400000);
        } catch (TransactionRejectedException exception) {
            System.out.println("Transaksi ditolak: " + exception.getMessage());
        }

        System.out.println();
        account.printStatement();
    }
}
public class Transaction {
    private final TransactionType type;
    private final long amount;

    public Transaction(TransactionType type, long amount) {
        this.type = type;
        this.amount = amount;
    }

    public String toDisplayText() {
        return type + " Rp" + amount;
    }
}
public class TransactionRejectedException extends RuntimeException {
    public TransactionRejectedException(String message) {
        super(message);
    }
}
public enum TransactionType {
    DEPOSIT,
    WITHDRAWAL
}

Your simulator now enforces its business rules and explains rejected transactions clearly. Next, you will document the result and publish it as a portfolio project.

Publish Your Portfolio Project

Your Java simulator now protects its balance. Its transaction history keeps only valid activity.

Recruiters cannot review code that lives only on your Mac. This step turns your local work into a documented public portfolio project.

Git records a reviewable history. GitHub gives the finished repository a public home.

In this step, get ready to:
  • Document the simulator in README.md.
  • Commit the portfolio files on the main branch.
  • Publish a public GitHub repository.
Document the finished simulator

Markdown gives your repository a readable front page through README.md. The document explains what the simulator demonstrates before a reviewer opens the source files.

  • In the Visual Studio Code Explorer sidebar for bank-account-simulator, select the new-file control.
  • Name the file README.md.
  • Add the project overview by pasting this first section into README.md:
# Bank Account Transaction Simulator

A terminal-based Java project that models deposits, withdrawals, transaction history, and overdraft protection for a simple bank account.

## Features

- Create a bank account with an account number and owner
- Deposit and withdraw whole rupiah amounts
- Record successful transactions
- Reject non-positive amounts and overdrafts
- Print a readable account statement

## Concepts Practiced

- Classes, objects, constructors, and encapsulation
- Enums and ArrayList
- Business-rule validation
- Custom exceptions

## Requirements

- Oracle JDK 25

## Run

```shell
java Main.java
```

What does this documentation cover?

  • The opening description connects the simulator to banking rules that a reviewer can understand.
  • The feature list shows the observable behavior of the finished program.
  • The concepts section names the Java skills demonstrated by the source code.
  • The run section gives a reviewer the exact command needed to start the simulator.
  • Save README.md.
  • Use the editor toolbar's preview control to render README.md.

You'll see the project title above its purpose. The feature list appears below it as a readable portfolio summary.

Does the Markdown look unformatted?

Check that each heading starts with the same number of # characters shown above. Confirm that each list item starts on its own line.

Help me fix the formatting in my Java project README.

Expected output gives a reviewer a concrete result to compare with their own run. It also makes the overdraft protection visible from the repository page.

  • Return to README.md.
  • Place the cursor on a blank line below the existing closing code fence.
  • Add the documented terminal result by pasting:
## Expected Output

```text
Transaksi ditolak: Saldo Rp300000 tidak cukup untuk menarik Rp400000

Mutasi rekening NW-1001 - Nama Anda
DEPOSIT Rp500000
WITHDRAWAL Rp200000
Saldo akhir: Rp300000
```

Why include the expected output?

The output proves that the rejected withdrawal leaves the valid balance untouched. A reviewer can run the project before comparing their terminal with this example.

  • Save README.md.
  • Render the Markdown preview again.

You'll see a separate expected-output section. Its code block ends with Saldo akhir: Rp300000.

Is the output outside its code block?

Confirm that ```text appears immediately above the output. Confirm that a closing ``` appears on the final line.

Help me repair the expected-output code fence in README.md.

Java can generate compiled .class files while you work. A .gitignore rule keeps that generated bytecode out of the portfolio commit.

  • In the Explorer sidebar for bank-account-simulator, select the new-file control.
  • Name the file .gitignore.
  • Add the bytecode rule by pasting:
*.class

What does this rule do?

The *.class pattern excludes every filename ending in .class. Your repository stays focused on source code that another developer can inspect.

  • Save .gitignore.
  • Confirm that the Explorer sidebar lists README.md.
  • Confirm that the Explorer sidebar lists .gitignore.

Your workspace now contains the two portfolio files. The five Java source files remain unchanged.

Can't see the ignore file?

Confirm that the filename starts with a single dot. Check that it is spelled .gitignore with no extra extension.

Help me find or correct my .gitignore file in Visual Studio Code.

✔️ Awesome, I've got everything!

Great. Double check that README.md renders correctly. Confirm that .gitignore contains only the compiled-bytecode rule.

ⓧ I'd like to double check the full code

Compare every project file with these references. Each block shows the exact state needed before the first commit.

*.class

What should this file contain?

This single rule excludes compiled Java bytecode. No other ignore patterns are needed for this project.

import java.util.ArrayList;

public class Account {
    private final String accountNumber;
    private final String ownerName;
    private long balance;
    private final ArrayList<Transaction> transactions = new ArrayList<>();

    public Account(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
    }

    public void deposit(long amount) {
        validatePositiveAmount(amount);
        balance += amount;
        transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
    }

    public void withdraw(long amount) {
        validatePositiveAmount(amount);

        if (amount > balance) {
            throw new TransactionRejectedException(
                    "Saldo Rp" + balance + " tidak cukup untuk menarik Rp" + amount
            );
        }

        balance -= amount;
        transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
    }

    public long getBalance() {
        return balance;
    }

    public void printStatement() {
        System.out.println("Mutasi rekening " + accountNumber + " - " + ownerName);

        for (Transaction transaction : transactions) {
            System.out.println(transaction.toDisplayText());
        }

        System.out.println("Saldo akhir: Rp" + balance);
    }

    private void validatePositiveAmount(long amount) {
        if (amount <= 0) {
            throw new TransactionRejectedException("Nominal harus lebih besar dari nol");
        }
    }
}

What should this class do?

The account class owns the private balance. Its methods enforce transaction rules before updating the account history.

public class Main {
    public static void main(String[] args) {
        Account account = new Account("NW-1001", "Nama Anda");

        account.deposit(500000);
        account.withdraw(200000);

        try {
            account.withdraw(400000);
        } catch (TransactionRejectedException exception) {
            System.out.println("Transaksi ditolak: " + exception.getMessage());
        }

        System.out.println();
        account.printStatement();
    }
}

What should the program run?

The main method performs valid transactions before attempting an overdraft. It catches the rejection before printing the preserved account statement.

# Bank Account Transaction Simulator

A terminal-based Java project that models deposits, withdrawals, transaction history, and overdraft protection for a simple bank account.

## Features

- Create a bank account with an account number and owner
- Deposit and withdraw whole rupiah amounts
- Record successful transactions
- Reject non-positive amounts and overdrafts
- Print a readable account statement

## Concepts Practiced

- Classes, objects, constructors, and encapsulation
- Enums and ArrayList
- Business-rule validation
- Custom exceptions

## Requirements

- Oracle JDK 25

## Run

```shell
java Main.java
```

## Expected Output

```text
Transaksi ditolak: Saldo Rp300000 tidak cukup untuk menarik Rp400000

Mutasi rekening NW-1001 - Nama Anda
DEPOSIT Rp500000
WITHDRAWAL Rp200000
Saldo akhir: Rp300000
```

What should the README explain?

The README presents the project purpose. It also gives reviewers the requirements and run command before showing the expected result.

public class Transaction {
    private final TransactionType type;
    private final long amount;

    public Transaction(TransactionType type, long amount) {
        this.type = type;
        this.amount = amount;
    }

    public String toDisplayText() {
        return type + " Rp" + amount;
    }
}

What should each transaction store?

Each transaction keeps its type and amount. Its display method turns those values into one statement line.

public class TransactionRejectedException extends RuntimeException {
    public TransactionRejectedException(String message) {
        super(message);
    }
}

What should this exception represent?

This unchecked exception carries the business-rule rejection message. The main method can catch it without changing the valid account state.

public enum TransactionType {
    DEPOSIT,
    WITHDRAWAL
}

What should this enum define?

The enum limits recorded transactions to deposits or withdrawals. Those fixed values keep statement entries consistent.

Commit the project with Git

A Git commit captures the exact state of your portfolio project. Before creating it, you need to confirm which repository initialization command your installed Git version supports.

  • Switch back to the integrated terminal from earlier.
  • Check the installed Git version by running this command:
git --version

What does this command show?

The command prints the Git version installed on your Mac. That version determines how you create the initial main branch.

You'll see a Git version number in the terminal. Choose the matching path below.

✔️ I see a current version

Git 2.28.0 or later supports selecting the initial branch during repository creation.

  • Initialize the workspace on main by running this command:
git init -b main

What does this command do?

The command creates the local Git repository. The -b main option makes main its first branch.

You'll see confirmation that an empty Git repository was initialized inside bank-account-simulator.

ⓧ I see an older version

A Git version below 2.28.0 uses the documented fallback to name the initial branch.

  • Initialize the repository on main by running this command:
git init && git symbolic-ref HEAD refs/heads/main

What does this fallback do?

The first command creates the repository. The second command points its initial branch at main.

You'll see confirmation that an empty Git repository was initialized inside bank-account-simulator.

Staging selects the files that belong in the commit. Your .gitignore rule prevents compiled bytecode from entering that selection.

  • Stage the portfolio files by running this command:
git add .

What does this command do?

The command stages the files inside bank-account-simulator. Git applies the ignore rule while building that staged snapshot.

Before you commit, picture the files you expect the commit summary to include.

  • Create the portfolio commit by running this command:
git commit -m "Build bank account transaction simulator"

What does this command do?

The command saves the staged snapshot with the message Build bank account transaction simulator. That message describes the completed result in the repository history.

You'll see a commit summary for seven created files. No .class file should appear in that summary.

Did the commit stop?

If Git asks for your author identity, follow the configuration instructions printed in the terminal. Run the commit command again after saving that identity.

If Git reports that no changes are available, confirm that the terminal is inside bank-account-simulator.

Help me diagnose why my first Git commit did not complete.

Publish the repository on GitHub

The local commit is ready to share. An empty public repository gives Git a destination without introducing files that could conflict with your existing project.

  • In your browser, sign in to GitHub.
  • Select New repository.
  • Enter bank-account-simulator in the repository name field.
  • Choose Public as the repository visibility.
  • Leave the README initialization option unselected.
  • Leave the license initialization option unselected.
  • Leave the gitignore initialization option unselected.

Why keep the repository empty?

Your local commit already contains the finished files. An empty repository accepts that history without creating a competing first commit.

  • Click Create repository.

You'll see setup instructions for the empty repository. This confirms that the public destination is ready.

  • Copy the repository's remote URL into REMOTE-URL.
  • Switch back to the integrated terminal from earlier.
  • Configure the copied repository as origin by running:
git remote add origin [[REMOTE_URL="REMOTE-URL"]]

What does this command do?

The command stores your GitHub repository URL under the short name origin. Future push commands can use that name instead of repeating the URL.

  • Verify the configured remote by running this command:
git remote -v

What does this check prove?

The command displays the URL Git uses for remote operations. You'll see your copied repository URL listed twice.

Does the remote URL look wrong?

Compare the displayed URL with the repository page. Confirm that it ends with the bank-account-simulator repository name.

Help me diagnose an incorrect Git origin URL.

The first push can trigger GitHub authentication. The prompt checks whether your account can write to the public repository.

Before you push, picture the seven tracked files you expect to see on GitHub.

  • Push the main branch by running this command:
git push -u origin main

What does this command do?

The command uploads your local main branch to GitHub. The -u option connects the local branch with its remote counterpart for future pushes.

  • Approve access with your existing GitHub account if an authentication prompt appears.

You'll see output confirming that main was pushed. The output also confirms that the local branch tracks the remote branch.

Did the push fail?

Confirm that the repository name shown by git remote -v matches the public repository you created. Complete any GitHub authentication request before retrying the push.

Help me troubleshoot my first push to GitHub.

  • Return to the public bank-account-simulator repository page.
  • Refresh the repository page.
  • Confirm that README.md appears on the main branch.
  • Confirm that .gitignore appears on the main branch.
  • Confirm that all five Java source files appear on the main branch.

You'll see the rendered README below the repository files. Your run instructions and preserved-balance output are now visible to reviewers.

Strong finish. Your banking simulator is now documented and publicly reviewable as a Java portfolio project.

Secret mission

Secret Mission: Transfer Money Between Accounts

A safe transfer must update two accounts without leaving their balances or transaction histories out of sync. Extend your simulator with a validated transfer that records matching entries for the source and destination accounts.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The local workspace plus your GitHub Free repository cost $0 to keep.

Resources you used:

  • Local bank-account-simulator workspace on your Mac.
  • Public GitHub bank-account-simulator repository on the main branch.

Keep everything running

No action is needed. Choose this option if you want the simulator ready for future changes.

  • Keep the local bank-account-simulator folder on your Mac.
  • Leave the public GitHub repository available for future work.

Pause - I'll come back to this later

No process or paid service stays active after the Java program exits. Your local files plus the GitHub repository remain ready for later.

  • Stop after your completed push to main.
  • Close Visual Studio Code when you finish.
  • Return to the same workspace when you want to continue.

Delete - I don't want to use this again

Deleting both copies is permanent. You can stop after either deletion if you decide to keep the other copy.

  • Move out of the project folder and delete bank-account-simulator by running:
cd ..
rm -rf bank-account-simulator

What Does This Remove?

The first line moves the terminal to the folder containing your workspace. The second line permanently removes the local bank-account-simulator folder.

  • Return to the public GitHub repository from earlier.
  • Select Settings.
  • Select General.
  • Scroll to Danger Zone.
  • Choose Delete this repository.
  • Review the warning acknowledgements.
  • Enter bank-account-simulator when GitHub asks you to confirm the repository name.
  • Complete the repository deletion.

The repository no longer appears in your GitHub account once deletion finishes.

Nice Work!

Nice Work!

You made it. Your published Java banking simulator now protects account balances across deposits, withdrawals, and two-account transfers.

You've learned how to:

  • Built a runnable banking simulator with classes, objects, constructors, private state, and methods that process whole rupiah transactions.
  • Used an enum and ArrayList to retain each successful transaction. Added business-rule validation and a custom exception after exposing the negative-balance bug.
  • Published a portfolio-ready repository on GitHub so reviewers can understand the project, run the program, and inspect the source code.
  • Completed the Secret Mission by adding a validated two-account transfer through transferTo(). Each transfer updates both balances and records matching history entries.

Ready to quiz yourself?