Build a Personal Budget Tracker in Java
Build a Java CLI budget tracker using OOP, file I/O, and Maven.
Introduction
30 Second Summary
Everyone tracks their spending somehow. Whether it's a spreadsheet, a banking app, or a mental note that fades by Tuesday, you need a system. What if you could build your own tool that works exactly the way you want?
In this project, you will build a personal budget tracker that runs in your terminal using Java. You'll model real-world transactions as objects, save them to a file, and package the whole thing with Maven.
What You'll Build
A command-line budget tracker that records income and expenses, calculates your balance, and persists data between sessions using Java's core object-oriented programming features.
By the end of this project, you'll have:
- An abstract class hierarchy with a base Transaction class and two subclasses (IncomeTransaction and ExpenseTransaction) demonstrating inheritance and polymorphism.
- A menu-driven console interface that lets you add income, add expenses, view your balance, and see transaction history.
- File-based persistence that saves your transactions to a text file and reloads them automatically when the app starts.
- Secret Mission: Add category-based spending summaries so you can see exactly where your money goes.
Are there any prerequisites?
None. This guide includes full setup instructions for Java, Maven, and your code editor. You don't need any prior programming experience to get started.
Install Java, Maven, and Set Up Your Editor
To build a Java application, you need the Java Development Kit (JDK) installed on your machine. The JDK gives you the javac compiler (to turn your code into something the computer can run) and the java runtime (to actually run it).
You also need Maven, a build tool that professional Java developers use to compile, test, and package their projects. In this step, you will install both tools and set up your code editor.
In this step, get ready to:
- Install JDK 25 and Maven using Homebrew.
- Verify your installation with version-check commands.
- Set up Cursor (or VS Code) with Java language support.
Install the JDK using Homebrew
Java comes in many versions. This project uses JDK 25, the latest Long-Term Support (LTS) release. LTS releases receive free updates for years, making them the safest choice for learning and production.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Check whether Java is already installed by running this command:
java --version
✔️ I see version 25
You already have JDK 25 installed. Skip ahead to the Maven installation section below.
ⓧ I see an older version
You have Java installed, but it is an older version. Install the correct version by running this command:
brew install openjdk@25
Homebrew installs openjdk@25 as "keg-only", meaning it is not automatically available on your PATH. You need to create a symlink so your system can find it.
- Register the JDK with your system by running:
sudo ln -sfn $HOMEBREW_PREFIX/opt/openjdk@25/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-25.jdk
What does this command do?
The sudo ln -sfn command creates a symbolic link (a shortcut) from where Homebrew installed the JDK to the directory where macOS looks for Java installations. Without this link, commands like java and javac will not find the new JDK.
- Your terminal will prompt you for your computer password. Type it in (you will not see characters appear) and press Enter.
- Add the JDK to your PATH by running:
echo 'export PATH="$HOMEBREW_PREFIX/opt/openjdk@25/bin:$PATH"' >> ~/.zshrc
- Reload your shell configuration by running:
source ~/.zshrc
- Verify the new version by running:
java --version
You should see output that includes 25.0.3 in the version number.
ⓧ Command not found
Java is not installed on your machine yet. Install it with Homebrew by running this command:
brew install openjdk@25
Homebrew installs openjdk@25 as "keg-only", meaning it is not automatically available on your PATH. You need to create a symlink so your system can find it.
- Register the JDK with your system by running:
sudo ln -sfn $HOMEBREW_PREFIX/opt/openjdk@25/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-25.jdk
- Your terminal will prompt you for your computer password. Type it in (you will not see characters appear) and press Enter.
- Add the JDK to your PATH by running:
echo 'export PATH="$HOMEBREW_PREFIX/opt/openjdk@25/bin:$PATH"' >> ~/.zshrc
- Reload your shell configuration by running:
source ~/.zshrc
- Verify the installation by running:
java --version
You should see output that includes 25.0.3 in the version number.
Still seeing command not found?
Make sure Homebrew itself is installed. Run brew --version to check. If Homebrew is not installed, visit https://brew.sh and follow the installation instructions.
help me install Homebrew and Java on my Mac
- Also verify that the Java compiler is available by running:
javac --version
You should see a version number matching 25.0.3.
What is a JDK?
The JDK (Java Development Kit) bundles everything you need to write and run Java programs. It includes the javac compiler, the java runtime, and standard libraries.
JDK 25 is the latest Long-Term Support release. LTS versions receive free updates until at least September 2028, making them stable for production use.
Install Maven
Maven is Java's standard build tool. It automates compilation, testing, and packaging so you do not have to run commands manually for each file.
- Install Maven by running:
brew install maven
- Verify the installation by running:
mvn -v
✔️ I see Maven version info
You should see output that includes Apache Maven 3.9.14 and the Java version it is using. Maven is ready to go.
ⓧ Command not found or an error
If mvn is not found, the Homebrew installation may not have completed. Try running brew install maven again and check for error messages.
If you see a Java-related error, make sure your JDK installation from the previous substep completed successfully.
help me troubleshoot my Maven installation
Why Maven and not just javac?
You will start this project by compiling with javac directly so you can see what happens under the hood. Later, you will migrate to Maven, which automates compilation, handles dependencies, and packages your app into a distributable JAR file.
This two-phase approach helps you understand what build tools actually do for you.
Set up your code editor
You need a code editor with Java support to get syntax highlighting, error detection, and code completion as you type.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it. If you prefer VS Code, type Visual Studio Code instead.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Search for Extension Pack for Java in the search bar at the top of the panel.
- Click Install on the result published by Microsoft.
What does this extension pack include?
The Extension Pack for Java bundles several tools: language support (syntax highlighting, error checking), a debugger, test runner, Maven integration, and IntelliSense (code completion). It turns your editor into a full Java development environment.
- Once the installation finishes, you are ready to write Java code. Now create your project folder.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir budget-tracker
cd budget-tracker
- Confirm the folder was created by running:
ls ~/Desktop
You should see budget-tracker in the output.
- Open this folder in your editor by running:
cursor .
Using VS Code instead?
If you are using VS Code, run code . instead of cursor . to open the folder.
Command not recognized?
If cursor or code is not recognized, open the editor manually and use File then Open Folder to select the budget-tracker folder on your Desktop.
help me open my project folder in Cursor or VS Code
Your development environment is ready. Next up, you will create the class hierarchy that models income and expense transactions.
Create the Transaction Class Hierarchy
Your Java environment is ready. Now it's time to write your first Java code and design the core data model for your budget tracker.
Every transaction in a budget tracker shares common traits: an amount, a description, and a date. But income and expenses behave differently when displayed or saved. Inheritance lets you define shared behavior once in a parent class, then customize it in subclasses.
In this step, get ready to:
- Write an abstract Transaction base class with shared fields and methods.
- Create IncomeTransaction and ExpenseTransaction subclasses that override behavior.
- Compile the class hierarchy to verify everything links together.
Create the project folder structure
Your budget tracker needs three Java source files that form a class hierarchy. The parent class holds shared logic, and two child classes customize behavior for income vs. expenses.
- In Cursor, confirm that your budget-tracker folder is open in the Explorer sidebar on the left.
- Open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Confirm you're inside the budget-tracker folder by running:
pwd
You should see a path ending in budget-tracker. If not, navigate there with cd ~/Desktop/budget-tracker.
Write the abstract Transaction base class
An abstract class is a blueprint that other classes must extend. It defines shared structure and behavior, but forces subclasses to provide their own implementation for certain methods.
- In the Explorer sidebar, right-click inside the file list and select New File.
- Name the file Transaction.java and press Enter.
- Add the following code to Transaction.java:
import java.time.LocalDate;
public abstract class Transaction {
private double amount;
private String description;
private LocalDate date;
public Transaction(double amount, String description) {
this.amount = amount;
this.description = description;
this.date = LocalDate.now();
}
public Transaction(double amount, String description, LocalDate date) {
this.amount = amount;
this.description = description;
this.date = date;
}
public double getAmount() {
return amount;
}
public String getDescription() {
return description;
}
public LocalDate getDate() {
return date;
}
public abstract String getSummary();
public abstract String toFileString();
@Override
public String toString() {
return String.format("%s %s", date, getSummary());
}
}
What does this code do?
This abstract class defines the shared skeleton for every transaction in your budget tracker.
- Three private fields (amount, description, date) store each transaction's data. Encapsulation keeps them hidden from outside code.
- Two constructors let you create a transaction with today's date (for new entries) or a specific date (for loading saved data).
- Public getter methods (getAmount(), getDescription(), getDate()) provide read-only access to those fields.
- The abstract keyword on getSummary() and toFileString() means subclasses MUST provide their own version of these methods.
- Save Transaction.java.
- Compile the file to confirm it's valid Java by running:
javac Transaction.java
You should see no output at all. Silence from the Java compiler means success.
Seeing a compiler error?
- Check that the filename is exactly Transaction.java with a capital T. Java requires the filename to match the class name.
- Make sure the import java.time.LocalDate; line is at the very top of the file, before the class declaration.
- Confirm your terminal is inside the budget-tracker folder where the file lives.
Still stuck? Help me fix a compiler error in Transaction.java
✔️ Awesome, I've got everything!
Great. Make sure you've saved Transaction.java before moving on.
ⓧ I'd like to double check the full code
import java.time.LocalDate;
public abstract class Transaction {
private double amount;
private String description;
private LocalDate date;
public Transaction(double amount, String description) {
this.amount = amount;
this.description = description;
this.date = LocalDate.now();
}
public Transaction(double amount, String description, LocalDate date) {
this.amount = amount;
this.description = description;
this.date = date;
}
public double getAmount() {
return amount;
}
public String getDescription() {
return description;
}
public LocalDate getDate() {
return date;
}
public abstract String getSummary();
public abstract String toFileString();
@Override
public String toString() {
return String.format("%s %s", date, getSummary());
}
}
Create the income and expense subclasses
Now you'll create two concrete classes that extend Transaction. Each subclass provides its own implementation of getSummary() and toFileString(). This is polymorphism in action: same method name, different behavior depending on the object type.
- Create a new file called IncomeTransaction.java in the same folder.
- Add the following code to IncomeTransaction.java:
import java.time.LocalDate;
public class IncomeTransaction extends Transaction {
public IncomeTransaction(double amount, String description) {
super(amount, description);
}
public IncomeTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("+$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "income|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
What does this code do?
- The extends Transaction keyword makes this a subclass that inherits all of Transaction's fields and methods.
- The super(amount, description) call passes arguments up to the parent constructor to set the shared fields.
- The getSummary() override formats income with a +$ prefix so it's visually distinct from expenses.
- The toFileString() override produces a pipe-delimited line like income|50.0|Salary|2026-05-27 for saving to a file.
- Save IncomeTransaction.java.
✔️ Awesome, I've got everything!
Great. Next up is the matching ExpenseTransaction class.
ⓧ I'd like to double check the full code
import java.time.LocalDate;
public class IncomeTransaction extends Transaction {
public IncomeTransaction(double amount, String description) {
super(amount, description);
}
public IncomeTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("+$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "income|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
- Create a new file called ExpenseTransaction.java in the same folder.
- Add the following code to ExpenseTransaction.java:
import java.time.LocalDate;
public class ExpenseTransaction extends Transaction {
public ExpenseTransaction(double amount, String description) {
super(amount, description);
}
public ExpenseTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("-$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "expense|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
What does this code do?
This class mirrors IncomeTransaction but customizes behavior for money going out.
- The getSummary() override uses a -$ prefix instead of +$.
- The toFileString() override writes expense| as the type tag so the app can tell income from expenses when loading saved data.
- Save ExpenseTransaction.java.
- Compile all three files together to confirm the hierarchy is valid by running:
javac *.java
No output means all three files compiled successfully. The Java compiler found no errors in your class hierarchy.
Seeing a compiler error?
- If you see cannot find symbol, make sure Transaction.java is in the same folder as the subclass files. The subclasses depend on the parent.
- Check that extends Transaction is spelled exactly right in both subclass files. Java is case-sensitive.
- Verify that both getSummary() and toFileString() are implemented in each subclass. Missing either one causes an error because the parent declares them as abstract.
Still stuck? Help me fix a compiler error in my Transaction class hierarchy
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
ⓧ I'd like to double check the full code
import java.time.LocalDate;
public class ExpenseTransaction extends Transaction {
public ExpenseTransaction(double amount, String description) {
super(amount, description);
}
public ExpenseTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("-$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "expense|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
Your class hierarchy is locked in. You have a clean separation between shared behavior (in the parent) and custom behavior (in each subclass). Next up, you'll build the BudgetTracker class that manages a collection of these transactions and handles saving them to a file.
Build the BudgetTracker Class
Your class hierarchy is in place. You have an abstract Transaction class and two concrete subclasses that know how to describe themselves.
Now you need something to manage those transactions. The BudgetTracker class will hold a list of transactions, calculate balances, and save everything to a file so your data survives between sessions.
In this step, get ready to:
- Create the BudgetTracker class with an ArrayList to store transactions.
- Add methods to add transactions, calculate balance, and list transactions.
- Add file I/O methods to save and load transactions from a text file.
Create the BudgetTracker class and fields
This class is where polymorphism comes to life. You will store both IncomeTransaction and ExpenseTransaction objects in a single ArrayList<Transaction>. Java lets you do this because both subclasses extend Transaction.
- Create a new file called BudgetTracker.java in your budget-tracker folder, then paste this code into it:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
public class BudgetTracker {
// Stores all transactions (both income and expense)
private ArrayList<Transaction> transactions;
// File name for saving and loading data
private static final String FILE_NAME = "transactions.txt";
public BudgetTracker() {
transactions = new ArrayList<>();
// Load any previously saved transactions
loadFromFile();
}
}
What does this code do?
The imports bring in everything you need for file reading, file writing, dates, and dynamic lists.
The ArrayList<Transaction> field is the key to polymorphism here. Even though the list is typed as Transaction, it can hold any object whose class extends Transaction. Both IncomeTransaction and ExpenseTransaction fit.
The constructor creates an empty list and immediately tries to load saved data from transactions.txt. You will write the loadFromFile() method later in this step.
- Save BudgetTracker.java.
- Confirm the file exists by running:
ls
You should see BudgetTracker.java alongside your three transaction files.
Don't see BudgetTracker.java?
Make sure you saved the file inside the budget-tracker folder, not in a parent or child directory. Run pwd to check you are in the right folder.
help me find where my BudgetTracker.java file was saved
Add transaction and balance methods
With the class skeleton ready, you need methods that let users add income, add expenses, and check their balance.
- In BudgetTracker.java, find the closing } of the constructor, then add the following methods below it (but still inside the class):
public void addIncome(double amount, String description) {
transactions.add(new IncomeTransaction(amount, description));
saveToFile();
System.out.println("Income added successfully!");
}
public void addExpense(double amount, String description) {
transactions.add(new ExpenseTransaction(amount, description));
saveToFile();
System.out.println("Expense added successfully!");
}
public double getBalance() {
double balance = 0;
// Check each transaction's type to decide add or subtract
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
balance += t.getAmount();
} else {
balance -= t.getAmount();
}
}
return balance;
}
public double getTotalIncome() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
total += t.getAmount();
}
}
return total;
}
public double getTotalExpenses() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof ExpenseTransaction) {
total += t.getAmount();
}
}
return total;
}
What does this code do?
- addIncome() and addExpense() create the appropriate subclass object, add it to the list, save to file, and print a confirmation.
- getBalance() uses instanceof to check each transaction's actual type at runtime. Income adds to the balance, expenses subtract from it.
- getTotalIncome() and getTotalExpenses() filter the list by type and sum up only the matching transactions.
What is instanceof?
The instanceof keyword checks whether an object is an instance of a specific class. Because your list holds Transaction references, Java does not automatically know if a particular entry is income or expense. instanceof lets you ask at runtime.
- Save BudgetTracker.java.
You won't see a visible change yet. The next chunk adds display methods, then file I/O completes the class.
- Below the getTotalExpenses() method, add these display methods:
public void showTransactions() {
if (transactions.isEmpty()) {
System.out.println("No transactions recorded yet.");
return;
}
System.out.println("\n--- Transaction History ---");
// Each transaction prints itself using its overridden toString()
for (Transaction t : transactions) {
System.out.println(t);
}
System.out.println("---------------------------");
}
public void showBalance() {
System.out.println("\n--- Balance Summary ---");
System.out.printf("Total Income: $%.2f%n", getTotalIncome());
System.out.printf("Total Expenses: $%.2f%n", getTotalExpenses());
System.out.printf("Balance: $%.2f%n", getBalance());
System.out.println("-----------------------");
}
What does this code do?
- showTransactions() prints every transaction by calling System.out.println(t). Java automatically calls each object's toString() method, which you defined in the abstract Transaction class.
- showBalance() prints a formatted summary using printf for consistent dollar formatting. The %.2f format specifier shows exactly two decimal places.
- Save BudgetTracker.java.
You won't see a change yet. The next substep adds file I/O, and then you can compile the whole project.
Add file I/O methods to save and load transactions
Your budget tracker needs to remember data between sessions. These two private methods handle writing transactions to a text file and reading them back in.
- Below the showBalance() method, add these two private methods:
private void saveToFile() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME))) {
// Write each transaction as a pipe-delimited line
for (Transaction t : transactions) {
writer.write(t.toFileString());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Error saving transactions: " + e.getMessage());
}
}
private void loadFromFile() {
File file = new File(FILE_NAME);
if (!file.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
// Split each line by the pipe character
String[] parts = line.split("\\|");
if (parts.length == 4) {
String type = parts[0];
double amount = Double.parseDouble(parts[1]);
String description = parts[2];
LocalDate date = LocalDate.parse(parts[3]);
if (type.equals("income")) {
transactions.add(new IncomeTransaction(amount, description, date));
} else {
transactions.add(new ExpenseTransaction(amount, description, date));
}
}
}
} catch (IOException e) {
System.out.println("Error loading transactions: " + e.getMessage());
}
}
What does this code do?
- saveToFile() loops through every transaction, calls toFileString() on each one (which produces the pipe-delimited format like income|50.0|Salary|2026-05-27), and writes it to transactions.txt.
- loadFromFile() reads each line from the file, splits it on the | character, and reconstructs the correct subclass object using the saved date.
- The try-with-resources syntax (the try (...) pattern) ensures the file is automatically closed even if an error occurs.
Why pipe-delimited instead of CSV?
Transaction descriptions might contain commas (like "Lunch, coffee"). Using the pipe character | as a delimiter avoids accidentally splitting a description into multiple fields.
- Save BudgetTracker.java.
- Verify the full project compiles by running:
javac *.java
The terminal should return with no output. That means compilation succeeded and Java generated .class files for each of your source files.
Seeing a compilation error?
Check that your BudgetTracker.java has the closing } brace for the class at the very end of the file. Every method must be inside the class braces.
If you see "cannot find symbol" errors, make sure all four .java files are in the same budget-tracker folder. The compiler needs them all together.
help me fix my BudgetTracker.java compilation error
✔️ Awesome, I've got everything!
Great. Double check you have saved BudgetTracker.java and that javac *.java completed without errors.
ⓧ I'd like to double check the full code
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
public class BudgetTracker {
private ArrayList<Transaction> transactions;
private static final String FILE_NAME = "transactions.txt";
public BudgetTracker() {
transactions = new ArrayList<>();
loadFromFile();
}
public void addIncome(double amount, String description) {
transactions.add(new IncomeTransaction(amount, description));
saveToFile();
System.out.println("Income added successfully!");
}
public void addExpense(double amount, String description) {
transactions.add(new ExpenseTransaction(amount, description));
saveToFile();
System.out.println("Expense added successfully!");
}
public double getBalance() {
double balance = 0;
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
balance += t.getAmount();
} else {
balance -= t.getAmount();
}
}
return balance;
}
public double getTotalIncome() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
total += t.getAmount();
}
}
return total;
}
public double getTotalExpenses() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof ExpenseTransaction) {
total += t.getAmount();
}
}
return total;
}
public void showTransactions() {
if (transactions.isEmpty()) {
System.out.println("No transactions recorded yet.");
return;
}
System.out.println("\n--- Transaction History ---");
for (Transaction t : transactions) {
System.out.println(t);
}
System.out.println("---------------------------");
}
public void showBalance() {
System.out.println("\n--- Balance Summary ---");
System.out.printf("Total Income: $%.2f%n", getTotalIncome());
System.out.printf("Total Expenses: $%.2f%n", getTotalExpenses());
System.out.printf("Balance: $%.2f%n", getBalance());
System.out.println("-----------------------");
}
private void saveToFile() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME))) {
for (Transaction t : transactions) {
writer.write(t.toFileString());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Error saving transactions: " + e.getMessage());
}
}
private void loadFromFile() {
File file = new File(FILE_NAME);
if (!file.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\|");
if (parts.length == 4) {
String type = parts[0];
double amount = Double.parseDouble(parts[1]);
String description = parts[2];
LocalDate date = LocalDate.parse(parts[3]);
if (type.equals("income")) {
transactions.add(new IncomeTransaction(amount, description, date));
} else {
transactions.add(new ExpenseTransaction(amount, description, date));
}
}
}
} catch (IOException e) {
System.out.println("Error loading transactions: " + e.getMessage());
}
}
}
Your BudgetTracker class is complete. It can store, calculate, display, save, and load transactions. Next up, you will write the menu-driven interface that lets a user interact with all of this through the terminal.
Create the Menu-Driven Interface
You have a full transaction class hierarchy and a BudgetTracker engine that can store, calculate, and persist data. But there is no way for a user to actually interact with any of it.
In this step, you will create Main.java, the entry point of your application. It displays a menu, reads the user's choice, and routes each option to the right method on your tracker.
In this step, get ready to:
- Write a menu loop that displays options and reads user input.
- Add helper methods with input validation for income and expenses.
- Compile and run the complete application.
Write the menu loop in Main.java
Java applications start from a main() method. Yours will create a BudgetTracker instance and loop forever, showing a numbered menu and waiting for the user to pick an option.
- Create a new file called Main.java inside your budget-tracker folder.
- Paste the following code into Main.java:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// Create the tracker and a scanner for reading input
BudgetTracker tracker = new BudgetTracker();
Scanner scanner = new Scanner(System.in);
boolean running = true;
System.out.println("=== Personal Budget Tracker ===");
// Main loop: show menu, read choice, act on it
while (running) {
System.out.println("\nWhat would you like to do?");
System.out.println("1. Add income");
System.out.println("2. Add expense");
System.out.println("3. View balance");
System.out.println("4. View transaction history");
System.out.println("5. Quit");
System.out.print("Choose an option (1-5): ");
String choice = scanner.nextLine().trim();
// Route user's choice to the right action
switch (choice) {
case "1":
addIncome(scanner, tracker);
break;
case "2":
addExpense(scanner, tracker);
break;
case "3":
tracker.showBalance();
break;
case "4":
tracker.showTransactions();
break;
case "5":
running = false;
System.out.println("Goodbye! Your data has been saved.");
break;
default:
System.out.println("Invalid option. Please choose 1-5.");
}
}
scanner.close();
}
}
What does this code do?
This is the heart of your application's user interface.
- Scanner reads text from the terminal. scanner.nextLine() waits for the user to type something and press Enter.
- The while (running) loop keeps the menu showing until the user picks option 5, which sets running to false.
- The switch statement routes each choice to the matching method on your BudgetTracker instance.
- Options 1 and 2 call helper methods (addIncome and addExpense) that you will write next.
You won't see a change yet. The file will not compile until you add the helper methods in the next chunk.
Add the income input helper
The menu loop calls addIncome(scanner, tracker), but that method does not exist yet. It needs to prompt the user for a dollar amount and a description, validate both inputs, then pass them to the tracker.
- In Main.java, find the closing } of the class (the very last line).
- Add the following method just above that closing brace:
private static void addIncome(Scanner scanner, BudgetTracker tracker) {
// Prompt for a dollar amount
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
// Prompt for a description
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
// Pass validated input to the tracker
tracker.addIncome(amount, description);
}
What does this code do?
- Double.parseDouble() converts the user's text into a number. If they typed letters instead, Java throws a NumberFormatException which the catch block handles gracefully.
- The early return statements exit the method without adding anything. This prevents bad data from reaching your tracker.
- Only after both validations pass does the method call tracker.addIncome(), which adds the transaction and saves to file.
You won't see a change yet. The file still needs the addExpense method before it will compile.
Add the expense input helper
The addExpense helper follows the same pattern as addIncome. It validates the amount and description, then calls the tracker's expense method.
- Add the following method directly below addIncome, still inside the class:
private static void addExpense(Scanner scanner, BudgetTracker tracker) {
// Prompt for a dollar amount
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
// Prompt for a description
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
// Pass validated input to the tracker
tracker.addExpense(amount, description);
}
Why are addIncome and addExpense separate methods?
Right now they look nearly identical. Separating them keeps each method focused on one responsibility. As your app grows (like in the Secret Mission), you can add category selection to addExpense without touching the income flow.
- Save Main.java.
Seeing a red underline or error?
- Make sure both addIncome and addExpense are inside the Main class braces but outside the main() method braces.
- Check that you have exactly one closing } for the class at the very end of the file.
- Verify the method signatures match exactly: private static void addIncome(Scanner scanner, BudgetTracker tracker).
Help me fix a compile error in my Main.java file.
✔️ Awesome, I've got everything!
Great. Double check you have saved Main.java before moving on.
ⓧ I'd like to double check the full code
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
BudgetTracker tracker = new BudgetTracker();
Scanner scanner = new Scanner(System.in);
boolean running = true;
System.out.println("=== Personal Budget Tracker ===");
while (running) {
System.out.println("\nWhat would you like to do?");
System.out.println("1. Add income");
System.out.println("2. Add expense");
System.out.println("3. View balance");
System.out.println("4. View transaction history");
System.out.println("5. Quit");
System.out.print("Choose an option (1-5): ");
String choice = scanner.nextLine().trim();
switch (choice) {
case "1":
addIncome(scanner, tracker);
break;
case "2":
addExpense(scanner, tracker);
break;
case "3":
tracker.showBalance();
break;
case "4":
tracker.showTransactions();
break;
case "5":
running = false;
System.out.println("Goodbye! Your data has been saved.");
break;
default:
System.out.println("Invalid option. Please choose 1-5.");
}
}
scanner.close();
}
private static void addIncome(Scanner scanner, BudgetTracker tracker) {
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
tracker.addIncome(amount, description);
}
private static void addExpense(Scanner scanner, BudgetTracker tracker) {
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
tracker.addExpense(amount, description);
}
}
Compile and run your budget tracker
All five Java files are now complete. Time to compile them together and see your app in action.
- Compile all your Java source files by running this command:
javac *.java
- Run the application by running:
java Main
You should see the banner === Personal Budget Tracker === followed by your five menu options.
What do these commands do?
- javac *.java compiles every .java file in the current folder into .class bytecode files that the Java Virtual Machine can execute.
- java Main launches the JVM and tells it to run the main() method inside Main.class.
- Type 5 and press Enter to quit the app for now.
Seeing a compile error?
- If you see cannot find symbol, make sure all five files (Transaction.java, IncomeTransaction.java, ExpenseTransaction.java, BudgetTracker.java, Main.java) are in the same folder.
- If you see error: class Main is public, confirm the file is named exactly Main.java with a capital M.
- Run ls *.java to confirm all five source files are present in the current directory.
Help me fix a Java compile error in my budget tracker project.
Your budget tracker is alive. In the next step, you will put it through its paces by adding real transactions and verifying that everything persists correctly.
Test Your Budget Tracker
Your budget tracker is fully coded and compiled. Before migrating to Maven, you need to verify that everything actually works the way you designed it.
In this step, you'll run the application, feed it sample data, confirm the balance math is correct, and prove that your file I/O logic keeps data alive between sessions.
In this step, get ready to:
- Add sample income and expense entries.
- Verify the balance calculation is correct.
- Quit and restart the application to confirm data persistence.
Add sample income and expense entries
To test properly, you need at least one income and one expense so you can verify that the balance math works and both transaction types display correctly.
- Start the budget tracker by running this command:
java Main
You should see the menu appear with the heading === Personal Budget Tracker === followed by five numbered options.
- Type 1 and press Enter to add an income entry.
- When prompted for an amount, type 2500 and press Enter.
- When prompted for a description, type Salary and press Enter.
You should see the confirmation message Income added successfully! and the menu reappears.
- Type 2 and press Enter to add an expense.
- Enter 45 for the amount.
- Enter Groceries for the description.
You should see Expense added successfully! printed to the terminal.
- Add one more expense: type 2, then enter 120 for the amount and Electric bill for the description.
Seeing 'Invalid amount' or 'Description cannot be empty'?
These are your input validation checks working correctly. Make sure you enter a number (no dollar sign) for the amount and at least one character for the description.
Help me debug my budget tracker input
Verify the balance calculation is correct
With three transactions added ($2500 income, $45 expense, $120 expense), the expected balance is $2500 - $45 - $120 = $2335.
- Type 3 and press Enter to view your balance.
You should see a balance summary like this:
--- Balance Summary ---
Total Income: $2500.00
Total Expenses: $165.00
Balance: $2335.00
-----------------------
What is polymorphism doing here?
Your getBalance() method loops through a single ArrayList<Transaction> that holds both IncomeTransaction and ExpenseTransaction objects. It uses instanceof to decide whether to add or subtract each amount.
This is polymorphism in action. One list stores different types, and the program handles each type appropriately at runtime.
- Type 4 and press Enter to view your transaction history.
You should see all three transactions listed with today's date, each prefixed with +$ for income or -$ for expenses.
Quit and restart to confirm data persistence
The real test of your file I/O code is whether data survives after the program exits. Every time you add a transaction, saveToFile() writes all transactions to transactions.txt. When the app starts again, loadFromFile() reads them back in.
- Type 5 and press Enter to quit the application.
You should see Goodbye! Your data has been saved. and the terminal returns to the command prompt.
- Inspect the saved file by running this command:
cat transactions.txt
You should see three pipe-delimited lines, each containing the transaction type, amount, description, and date.
What does the file format look like?
Each line follows the pattern type|amount|description|date. For example: income|2500.0|Salary|2026-05-27. The date will reflect today's date.
The pipe character | is used as a delimiter instead of commas because descriptions might contain commas themselves.
- Start the budget tracker again by running:
java Main
- Type 4 and press Enter to view the transaction history.
You should see the same three transactions from your previous session. Your data persisted between runs.
- Type 3 to confirm the balance is still $2335.00.
- Type 5 to quit the application.
Transaction history is empty after restarting?
Check that transactions.txt exists in the same folder where you run java Main. If you changed directories between sessions, the app looks for the file in the current working directory.
Also confirm the file content looks correct with cat transactions.txt. Each line should have exactly 4 pipe-separated values. If a line has fewer, the parser skips it.
Help me fix my file persistence issue
Your budget tracker is fully functional. It tracks income and expenses, calculates balances with polymorphism, and persists data to a file. Next up, you'll migrate this project to a proper Maven structure so it builds and packages like a professional Java application.
Migrate to Maven
Your budget tracker compiles and runs perfectly from the command line. So far, you have been manually compiling with javac and running with java Main. This works for small projects, but professional Java teams rely on build tools to automate compilation, testing, and packaging.
Maven is the most widely used Java build tool in enterprise development. It enforces a standard directory layout, manages dependencies, and packages your code into a distributable JAR file. In this step, you'll restructure your project into Maven's layout and build an executable JAR.
In this step, get ready to:
- Create the standard Maven directory structure.
- Move your source files and add package declarations.
- Build and run the project as an executable JAR.
Create the standard Maven directory layout
Maven uses a "convention over configuration" approach. Application source code always lives in src/main/java. By following this layout, Maven knows exactly where to find your code without extra configuration.
- In your terminal, make sure you are inside the budget-tracker folder.
- Create the Maven source directory by running this command:
mkdir -p src/main/java/com/budgettracker
What does this command do?
The -p flag creates all the nested folders in one go. Maven expects your source files to live in a directory path that mirrors your package name.
The com/budgettracker part maps to the Java package com.budgettracker that you'll declare in each file.
- Confirm the directory was created by running:
ls src/main/java/com/budgettracker
The folder should exist (the output will be empty since no files are in it yet).
Move your source files into src/main/java and create a pom.xml
Now you'll move your Java source files into the Maven directory and add a package declaration so Java knows they belong to the com.budgettracker package.
- Move all your Java source files into the Maven directory by running this command:
mv *.java src/main/java/com/budgettracker/
- Remove the old compiled class files by running:
rm *.class
Why remove the class files?
The .class files were generated by your earlier javac commands. Maven will recompile everything fresh into its own target/ directory, so these old files are no longer needed.
Next, you need to add a package declaration to the top of each Java file. When Java files live inside a named package directory, they must declare which package they belong to.
- Open src/main/java/com/budgettracker/Transaction.java in your editor.
- Find the first line of the file, which currently reads:
import java.time.LocalDate;
- Add package com.budgettracker; as a new first line above the imports, so the top of the file looks like this:
package com.budgettracker;
import java.time.LocalDate;
- Repeat this for the remaining four files. Open each file and add package com.budgettracker; as the very first line (followed by a blank line before the imports):
- Save all five files after adding the package declaration.
Why do I need a package declaration?
Java packages group related classes together and prevent naming conflicts. The declaration package com.budgettracker; tells the Java compiler that this class belongs to the com.budgettracker package.
The directory structure must match the package name exactly. That's why your files live in src/main/java/com/budgettracker/ — each folder maps to a segment of the package name.
Getting a compilation error later?
If Maven fails to compile, the most common cause is a missing or mistyped package declaration. Check that every Java file starts with exactly package com.budgettracker; on line 1 (not package com.budget_tracker; or similar).
Also confirm that nothing else appears above the package declaration. It must be the very first line.
help me fix a package declaration error in my Maven Java project
Now you need a pom.xml file at the project root. This is Maven's configuration file that defines your project's identity, compiler settings, and how to package it.
- Create a new file called pom.xml in the root of your budget-tracker folder (not inside src/).
- Paste the following content into pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.budgettracker</groupId>
<artifactId>budget-tracker</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Budget Tracker</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.budgettracker.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
What does this POM file do?
- groupId, artifactId, and version together form your project's unique identity in the Maven ecosystem.
- maven.compiler.release set to 25 tells Maven to compile against JDK 25.
- The maven-jar-plugin configuration sets com.budgettracker.Main as the entry point. This makes the JAR executable with java -jar.
- Save pom.xml.
✔️ Awesome, I've got everything!
Great. Double check you've saved pom.xml and all five Java files with their new package declarations.
ⓧ I'd like to double check the full code
Here is the complete pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.budgettracker</groupId>
<artifactId>budget-tracker</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Budget Tracker</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.budgettracker.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Here is Transaction.java (the only change is the package declaration on line 1):
package com.budgettracker;
import java.time.LocalDate;
public abstract class Transaction {
private double amount;
private String description;
private LocalDate date;
public Transaction(double amount, String description) {
this.amount = amount;
this.description = description;
this.date = LocalDate.now();
}
public Transaction(double amount, String description, LocalDate date) {
this.amount = amount;
this.description = description;
this.date = date;
}
public double getAmount() {
return amount;
}
public String getDescription() {
return description;
}
public LocalDate getDate() {
return date;
}
public abstract String getSummary();
public abstract String toFileString();
@Override
public String toString() {
return String.format("%s %s", date, getSummary());
}
}
Here is IncomeTransaction.java:
package com.budgettracker;
import java.time.LocalDate;
public class IncomeTransaction extends Transaction {
public IncomeTransaction(double amount, String description) {
super(amount, description);
}
public IncomeTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("+$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "income|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
Here is ExpenseTransaction.java:
package com.budgettracker;
import java.time.LocalDate;
public class ExpenseTransaction extends Transaction {
public ExpenseTransaction(double amount, String description) {
super(amount, description);
}
public ExpenseTransaction(double amount, String description, LocalDate date) {
super(amount, description, date);
}
@Override
public String getSummary() {
return String.format("-$%.2f %s", getAmount(), getDescription());
}
@Override
public String toFileString() {
return "expense|" + getAmount() + "|" + getDescription() + "|" + getDate();
}
}
Here is BudgetTracker.java:
package com.budgettracker;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
public class BudgetTracker {
private ArrayList<Transaction> transactions;
private static final String FILE_NAME = "transactions.txt";
public BudgetTracker() {
transactions = new ArrayList<>();
loadFromFile();
}
public void addIncome(double amount, String description) {
transactions.add(new IncomeTransaction(amount, description));
saveToFile();
System.out.println("Income added successfully!");
}
public void addExpense(double amount, String description) {
transactions.add(new ExpenseTransaction(amount, description));
saveToFile();
System.out.println("Expense added successfully!");
}
public double getBalance() {
double balance = 0;
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
balance += t.getAmount();
} else {
balance -= t.getAmount();
}
}
return balance;
}
public double getTotalIncome() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof IncomeTransaction) {
total += t.getAmount();
}
}
return total;
}
public double getTotalExpenses() {
double total = 0;
for (Transaction t : transactions) {
if (t instanceof ExpenseTransaction) {
total += t.getAmount();
}
}
return total;
}
public void showTransactions() {
if (transactions.isEmpty()) {
System.out.println("No transactions recorded yet.");
return;
}
System.out.println("\n--- Transaction History ---");
for (Transaction t : transactions) {
System.out.println(t);
}
System.out.println("---------------------------");
}
public void showBalance() {
System.out.println("\n--- Balance Summary ---");
System.out.printf("Total Income: $%.2f%n", getTotalIncome());
System.out.printf("Total Expenses: $%.2f%n", getTotalExpenses());
System.out.printf("Balance: $%.2f%n", getBalance());
System.out.println("-----------------------");
}
private void saveToFile() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME))) {
for (Transaction t : transactions) {
writer.write(t.toFileString());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Error saving transactions: " + e.getMessage());
}
}
private void loadFromFile() {
File file = new File(FILE_NAME);
if (!file.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\|");
if (parts.length == 4) {
String type = parts[0];
double amount = Double.parseDouble(parts[1]);
String description = parts[2];
LocalDate date = LocalDate.parse(parts[3]);
if (type.equals("income")) {
transactions.add(new IncomeTransaction(amount, description, date));
} else {
transactions.add(new ExpenseTransaction(amount, description, date));
}
}
}
} catch (IOException e) {
System.out.println("Error loading transactions: " + e.getMessage());
}
}
}
Here is Main.java:
package com.budgettracker;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
BudgetTracker tracker = new BudgetTracker();
Scanner scanner = new Scanner(System.in);
boolean running = true;
System.out.println("=== Personal Budget Tracker ===");
while (running) {
System.out.println("\nWhat would you like to do?");
System.out.println("1. Add income");
System.out.println("2. Add expense");
System.out.println("3. View balance");
System.out.println("4. View transaction history");
System.out.println("5. Quit");
System.out.print("Choose an option (1-5): ");
String choice = scanner.nextLine().trim();
switch (choice) {
case "1":
addIncome(scanner, tracker);
break;
case "2":
addExpense(scanner, tracker);
break;
case "3":
tracker.showBalance();
break;
case "4":
tracker.showTransactions();
break;
case "5":
running = false;
System.out.println("Goodbye! Your data has been saved.");
break;
default:
System.out.println("Invalid option. Please choose 1-5.");
}
}
scanner.close();
}
private static void addIncome(Scanner scanner, BudgetTracker tracker) {
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
tracker.addIncome(amount, description);
}
private static void addExpense(Scanner scanner, BudgetTracker tracker) {
System.out.print("Enter amount: $");
String amountInput = scanner.nextLine().trim();
double amount;
try {
amount = Double.parseDouble(amountInput);
if (amount <= 0) {
System.out.println("Amount must be greater than zero.");
return;
}
} catch (NumberFormatException e) {
System.out.println("Invalid amount. Please enter a number.");
return;
}
System.out.print("Enter description: ");
String description = scanner.nextLine().trim();
if (description.isEmpty()) {
System.out.println("Description cannot be empty.");
return;
}
tracker.addExpense(amount, description);
}
}
Build and run the project using mvn package and java -jar
Everything is in place. Maven can now compile your source files, package them into a JAR, and produce an executable artifact.
- Build the project by running this command from the budget-tracker folder:
mvn package
What does this command do?
The mvn package command runs Maven's default lifecycle phases in order: validate, compile, test, then package. It compiles all your Java source files and bundles the result into a JAR file inside the target/ directory.
- You should see output ending with BUILD SUCCESS and a line showing the path to target/budget-tracker-1.0-SNAPSHOT.jar.
Seeing BUILD FAILURE?
If Maven reports a compilation error, check that every Java file has package com.budgettracker; as the first line. Also verify that pom.xml is in the project root (same level as the src/ folder), not inside src/.
If you see an error about the Java version, confirm java --version shows 25. Maven needs JDK 25 installed because the POM sets maven.compiler.release to 25.
help me fix a Maven BUILD FAILURE in my Java budget tracker project
- Run your packaged application by running this command:
java -jar target/budget-tracker-1.0-SNAPSHOT.jar
You should see the familiar === Personal Budget Tracker === menu appear. Your app is now running from the packaged JAR file.
Why is this better than javac and java Main?
With javac, you had to manually compile every file. Maven handles compilation, testing, and packaging in one command. The resulting JAR is a single distributable file you can share with anyone who has Java installed.
Maven also standardizes your project layout so any Java developer can open it and immediately know where everything lives. This is the structure used by every major Java framework and IDE.
- Test the app by choosing option 3 to view your balance.
- Choose option 5 to quit.
Your transactions.txt file
The transactions.txt file from your earlier testing still lives in the project root. Since java -jar runs from the project root directory, the app finds and loads your existing data automatically.
Secret mission
Add Category Spending Summaries
Your budget tracker records expenses, but where is all your money actually going? Extend your app with spending categories so you can see exactly how much you spend on Food, Transport, Entertainment, and more.
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. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The budget-tracker project folder on your Desktop.
- The transactions.txt data file (inside your project folder).
- JDK 25 (installed via Homebrew).
- Maven (installed via Homebrew).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep Java and Maven available for future projects.
- Your budget-tracker folder stays on your Desktop.
- JDK 25 and Maven remain installed and ready for your next Java project.
- Your transactions.txt file keeps your budget data between sessions.
Pause - I'll come back to this later
There are no running processes to stop. The budget tracker only runs when you execute it manually, so there is nothing to pause.
- Your project files stay on disk until you're ready to come back.
- JDK 25 and Maven stay installed. They use disk space but no memory or CPU when idle.
- To resume later, open your terminal, navigate to the budget-tracker folder, and run java -jar target/budget-tracker-1.0-SNAPSHOT.jar.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your budget data permanently.
- Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/budget-tracker
- On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\budget-tracker
- If you no longer need Java, uninstall JDK 25 by running:
brew uninstall openjdk@25
- If you no longer need Maven, uninstall it by running:
brew uninstall maven
- Remove the JDK symlink you created earlier by running:
sudo rm -f /Library/Java/JavaVirtualMachines/openjdk-25.jdk
Nice Work!
Nice Work!
Nice work! You just built a fully functional personal budget tracker in Java, from scratch.
You've learned how to:
- Designed an OOP class hierarchy with an abstract base class, concrete subclasses using inheritance, and polymorphism to store different transaction types in a single ArrayList<Transaction>.
- Built a menu-driven console application with Scanner input, file I/O for data persistence, and control flow using loops, conditionals, and switch statements.
- Migrated the project to Maven with a standard directory layout, a pom.xml, and packaged it into a runnable JAR.
- Added category-based spending analysis using HashMap and instanceof to break down expenses by category.
Ready to quiz yourself?