Expense Tracker Desktop App in Java

A comprehensive, user-friendly desktop expense tracking application built with Java Swing and SQLite database.

Project Structure

ExpenseTracker/
├── src/
│   ├── com/expensetracker/
│   │   ├── models/
│   │   ├── dao/
│   │   ├── gui/
│   │   ├── services/
│   │   └── utils/
│   └── resources/
├── lib/
└── database/

Maven Dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.expensetracker</groupId>
<artifactId>expense-tracker</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SQLite Database -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
<!-- JFreeChart for Charts -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
<!-- Apache Commons for Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- JUnit for Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito for Testing -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.expensetracker.ExpenseTrackerApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

Domain Models

package com.expensetracker.models;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
public class Expense {
private Integer id;
private BigDecimal amount;
private String description;
private Category category;
private LocalDate date;
private PaymentMethod paymentMethod;
private String notes;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public enum PaymentMethod {
CASH("Cash"),
CREDIT_CARD("Credit Card"),
DEBIT_CARD("Debit Card"),
BANK_TRANSFER("Bank Transfer"),
DIGITAL_WALLET("Digital Wallet"),
OTHER("Other");
private final String displayName;
PaymentMethod(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
}
// Constructors
public Expense() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public Expense(BigDecimal amount, String description, Category category, 
LocalDate date, PaymentMethod paymentMethod, String notes) {
this();
this.amount = amount;
this.description = description;
this.category = category;
this.date = date;
this.paymentMethod = paymentMethod;
this.notes = notes;
}
// Getters and Setters
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Category getCategory() { return category; }
public void setCategory(Category category) { this.category = category; }
public LocalDate getDate() { return date; }
public void setDate(LocalDate date) { this.date = date; }
public PaymentMethod getPaymentMethod() { return paymentMethod; }
public void setPaymentMethod(PaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Expense expense = (Expense) o;
return Objects.equals(id, expense.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return String.format("Expense{id=%d, amount=%s, description='%s', category=%s}", 
id, amount, description, category);
}
}
package com.expensetracker.models;
import java.util.Objects;
public class Category {
private Integer id;
private String name;
private String description;
private Double budgetLimit;
private String colorCode;
// Constructors
public Category() {}
public Category(String name, String description, Double budgetLimit, String colorCode) {
this.name = name;
this.description = description;
this.budgetLimit = budgetLimit;
this.colorCode = colorCode;
}
public Category(String name) {
this(name, null, null, "#3498db");
}
// Getters and Setters
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getBudgetLimit() { return budgetLimit; }
public void setBudgetLimit(Double budgetLimit) { this.budgetLimit = budgetLimit; }
public String getColorCode() { return colorCode; }
public void setColorCode(String colorCode) { this.colorCode = colorCode; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Category category = (Category) o;
return Objects.equals(id, category.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return name;
}
}

Database Layer

package com.expensetracker.dao;
import com.expensetracker.models.Expense;
import com.expensetracker.models.Category;
import com.expensetracker.utils.DatabaseConnection;
import java.sql.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class ExpenseDAO {
public void createTable() {
String sql = """
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount DECIMAL(10,2) NOT NULL,
description TEXT NOT NULL,
category_id INTEGER NOT NULL,
date DATE NOT NULL,
payment_method TEXT NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories (id)
)
""";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
throw new RuntimeException("Error creating expenses table", e);
}
}
public Integer insert(Expense expense) {
String sql = """
INSERT INTO expenses (amount, description, category_id, date, payment_method, notes)
VALUES (?, ?, ?, ?, ?, ?)
""";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setBigDecimal(1, expense.getAmount());
pstmt.setString(2, expense.getDescription());
pstmt.setInt(3, expense.getCategory().getId());
pstmt.setDate(4, Date.valueOf(expense.getDate()));
pstmt.setString(5, expense.getPaymentMethod().name());
pstmt.setString(6, expense.getNotes());
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
throw new SQLException("Creating expense failed, no rows affected.");
}
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getInt(1);
} else {
throw new SQLException("Creating expense failed, no ID obtained.");
}
}
} catch (SQLException e) {
throw new RuntimeException("Error inserting expense", e);
}
}
public List<Expense> findAll() {
String sql = """
SELECT e.*, c.id as cat_id, c.name as cat_name, c.description as cat_description, 
c.budget_limit, c.color_code
FROM expenses e
JOIN categories c ON e.category_id = c.id
ORDER BY e.date DESC, e.created_at DESC
""";
List<Expense> expenses = new ArrayList<>();
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
expenses.add(mapRowToExpense(rs));
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching expenses", e);
}
return expenses;
}
public List<Expense> findByDateRange(LocalDate startDate, LocalDate endDate) {
String sql = """
SELECT e.*, c.id as cat_id, c.name as cat_name, c.description as cat_description, 
c.budget_limit, c.color_code
FROM expenses e
JOIN categories c ON e.category_id = c.id
WHERE e.date BETWEEN ? AND ?
ORDER BY e.date DESC, e.created_at DESC
""";
List<Expense> expenses = new ArrayList<>();
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDate(1, Date.valueOf(startDate));
pstmt.setDate(2, Date.valueOf(endDate));
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
expenses.add(mapRowToExpense(rs));
}
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching expenses by date range", e);
}
return expenses;
}
public List<Expense> findByCategory(Integer categoryId) {
String sql = """
SELECT e.*, c.id as cat_id, c.name as cat_name, c.description as cat_description, 
c.budget_limit, c.color_code
FROM expenses e
JOIN categories c ON e.category_id = c.id
WHERE e.category_id = ?
ORDER BY e.date DESC
""";
List<Expense> expenses = new ArrayList<>();
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, categoryId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
expenses.add(mapRowToExpense(rs));
}
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching expenses by category", e);
}
return expenses;
}
public boolean update(Expense expense) {
String sql = """
UPDATE expenses 
SET amount = ?, description = ?, category_id = ?, date = ?, 
payment_method = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setBigDecimal(1, expense.getAmount());
pstmt.setString(2, expense.getDescription());
pstmt.setInt(3, expense.getCategory().getId());
pstmt.setDate(4, Date.valueOf(expense.getDate()));
pstmt.setString(5, expense.getPaymentMethod().name());
pstmt.setString(6, expense.getNotes());
pstmt.setInt(7, expense.getId());
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("Error updating expense", e);
}
}
public boolean delete(Integer id) {
String sql = "DELETE FROM expenses WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("Error deleting expense", e);
}
}
public BigDecimal getTotalExpenses(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT SUM(amount) as total FROM expenses WHERE date BETWEEN ? AND ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDate(1, Date.valueOf(startDate));
pstmt.setDate(2, Date.valueOf(endDate));
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
BigDecimal total = rs.getBigDecimal("total");
return total != null ? total : BigDecimal.ZERO;
}
}
} catch (SQLException e) {
throw new RuntimeException("Error calculating total expenses", e);
}
return BigDecimal.ZERO;
}
private Expense mapRowToExpense(ResultSet rs) throws SQLException {
Expense expense = new Expense();
expense.setId(rs.getInt("id"));
expense.setAmount(rs.getBigDecimal("amount"));
expense.setDescription(rs.getString("description"));
expense.setDate(rs.getDate("date").toLocalDate());
expense.setPaymentMethod(Expense.PaymentMethod.valueOf(rs.getString("payment_method")));
expense.setNotes(rs.getString("notes"));
expense.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
expense.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
// Create category
Category category = new Category();
category.setId(rs.getInt("cat_id"));
category.setName(rs.getString("cat_name"));
category.setDescription(rs.getString("cat_description"));
category.setBudgetLimit(rs.getDouble("budget_limit"));
category.setColorCode(rs.getString("color_code"));
expense.setCategory(category);
return expense;
}
}
package com.expensetracker.dao;
import com.expensetracker.models.Category;
import com.expensetracker.utils.DatabaseConnection;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class CategoryDAO {
public void createTable() {
String sql = """
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
budget_limit REAL,
color_code TEXT DEFAULT '#3498db'
)
""";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
throw new RuntimeException("Error creating categories table", e);
}
}
public Integer insert(Category category) {
String sql = "INSERT INTO categories (name, description, budget_limit, color_code) VALUES (?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, category.getName());
pstmt.setString(2, category.getDescription());
pstmt.setObject(3, category.getBudgetLimit());
pstmt.setString(4, category.getColorCode());
int affectedRows = pstmt.executeUpdate();
if (affectedRows == 0) {
throw new SQLException("Creating category failed, no rows affected.");
}
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getInt(1);
} else {
throw new SQLException("Creating category failed, no ID obtained.");
}
}
} catch (SQLException e) {
throw new RuntimeException("Error inserting category", e);
}
}
public List<Category> findAll() {
String sql = "SELECT * FROM categories ORDER BY name";
List<Category> categories = new ArrayList<>();
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
categories.add(mapRowToCategory(rs));
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching categories", e);
}
return categories;
}
public Optional<Category> findById(Integer id) {
String sql = "SELECT * FROM categories WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapRowToCategory(rs));
}
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching category by ID", e);
}
return Optional.empty();
}
public Optional<Category> findByName(String name) {
String sql = "SELECT * FROM categories WHERE name = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapRowToCategory(rs));
}
}
} catch (SQLException e) {
throw new RuntimeException("Error fetching category by name", e);
}
return Optional.empty();
}
public boolean update(Category category) {
String sql = "UPDATE categories SET name = ?, description = ?, budget_limit = ?, color_code = ? WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, category.getName());
pstmt.setString(2, category.getDescription());
pstmt.setObject(3, category.getBudgetLimit());
pstmt.setString(4, category.getColorCode());
pstmt.setInt(5, category.getId());
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("Error updating category", e);
}
}
public boolean delete(Integer id) {
String sql = "DELETE FROM categories WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("Error deleting category", e);
}
}
private Category mapRowToCategory(ResultSet rs) throws SQLException {
Category category = new Category();
category.setId(rs.getInt("id"));
category.setName(rs.getString("name"));
category.setDescription(rs.getString("description"));
category.setBudgetLimit(rs.getDouble("budget_limit"));
if (rs.wasNull()) {
category.setBudgetLimit(null);
}
category.setColorCode(rs.getString("color_code"));
return category;
}
}

Database Connection Utility

package com.expensetracker.utils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
private static final String URL = "jdbc:sqlite:expense_tracker.db";
private static Connection connection = null;
private DatabaseConnection() {
// Private constructor to prevent instantiation
}
public static Connection getConnection() {
if (connection == null) {
try {
connection = DriverManager.getConnection(URL);
// Enable foreign keys
try (var stmt = connection.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
}
} catch (SQLException e) {
throw new RuntimeException("Error connecting to database", e);
}
}
return connection;
}
public static void closeConnection() {
if (connection != null) {
try {
connection.close();
connection = null;
} catch (SQLException e) {
System.err.println("Error closing database connection: " + e.getMessage());
}
}
}
public static void initializeDatabase() {
CategoryDAO categoryDAO = new CategoryDAO();
ExpenseDAO expenseDAO = new ExpenseDAO();
categoryDAO.createTable();
expenseDAO.createTable();
// Insert default categories if they don't exist
insertDefaultCategories(categoryDAO);
}
private static void insertDefaultCategories(CategoryDAO categoryDAO) {
String[] defaultCategories = {
"Food & Dining", "Transportation", "Shopping", "Entertainment", 
"Bills & Utilities", "Healthcare", "Education", "Travel", "Other"
};
String[] colors = {
"#e74c3c", "#3498db", "#9b59b6", "#f1c40f", 
"#1abc9c", "#e67e22", "#34495e", "#d35400", "#95a5a6"
};
for (int i = 0; i < defaultCategories.length; i++) {
String categoryName = defaultCategories[i];
if (categoryDAO.findByName(categoryName).isEmpty()) {
Category category = new Category(categoryName, null, null, colors[i]);
categoryDAO.insert(category);
}
}
}
}

Service Layer

package com.expensetracker.services;
import com.expensetracker.models.Expense;
import com.expensetracker.models.Category;
import com.expensetracker.dao.ExpenseDAO;
import com.expensetracker.dao.CategoryDAO;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ExpenseService {
private final ExpenseDAO expenseDAO;
private final CategoryDAO categoryDAO;
public ExpenseService() {
this.expenseDAO = new ExpenseDAO();
this.categoryDAO = new CategoryDAO();
}
public void addExpense(Expense expense) {
if (expense.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Expense amount must be positive");
}
if (expense.getDate().isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Expense date cannot be in the future");
}
expenseDAO.insert(expense);
}
public List<Expense> getAllExpenses() {
return expenseDAO.findAll();
}
public List<Expense> getExpensesByDateRange(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException("Start date cannot be after end date");
}
return expenseDAO.findByDateRange(startDate, endDate);
}
public List<Expense> getExpensesByCategory(Integer categoryId) {
return expenseDAO.findByCategory(categoryId);
}
public boolean updateExpense(Expense expense) {
return expenseDAO.update(expense);
}
public boolean deleteExpense(Integer expenseId) {
return expenseDAO.delete(expenseId);
}
public BigDecimal getTotalExpenses(LocalDate startDate, LocalDate endDate) {
return expenseDAO.getTotalExpenses(startDate, endDate);
}
public Map<Category, BigDecimal> getExpensesByCategory(LocalDate startDate, LocalDate endDate) {
List<Expense> expenses = getExpensesByDateRange(startDate, endDate);
return expenses.stream()
.collect(Collectors.groupingBy(
Expense::getCategory,
Collectors.reducing(
BigDecimal.ZERO,
Expense::getAmount,
BigDecimal::add
)
));
}
public List<Category> getAllCategories() {
return categoryDAO.findAll();
}
public void addCategory(Category category) {
if (categoryDAO.findByName(category.getName()).isPresent()) {
throw new IllegalArgumentException("Category with this name already exists");
}
categoryDAO.insert(category);
}
public boolean updateCategory(Category category) {
return categoryDAO.update(category);
}
public boolean deleteCategory(Integer categoryId) {
// Check if category has expenses
List<Expense> categoryExpenses = expenseDAO.findByCategory(categoryId);
if (!categoryExpenses.isEmpty()) {
throw new IllegalStateException("Cannot delete category with existing expenses");
}
return categoryDAO.delete(categoryId);
}
}

User Interface - Main Application

package com.expensetracker.gui;
import com.expensetracker.models.Expense;
import com.expensetracker.models.Category;
import com.expensetracker.services.ExpenseService;
import com.expensetracker.utils.DatabaseConnection;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;
public class ExpenseTrackerApp extends JFrame {
private final ExpenseService expenseService;
private JTable expensesTable;
private DefaultTableModel tableModel;
private JLabel totalLabel;
private JComboBox<String> monthComboBox;
private JComboBox<Integer> yearComboBox;
public ExpenseTrackerApp() {
this.expenseService = new ExpenseService();
initializeUI();
loadExpenses();
loadFilterData();
}
private void initializeUI() {
setTitle("Personal Expense Tracker");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1000, 700);
setLocationRelativeTo(null);
// Create main panel with border layout
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Create header panel
JPanel headerPanel = createHeaderPanel();
mainPanel.add(headerPanel, BorderLayout.NORTH);
// Create expenses table
JPanel tablePanel = createTablePanel();
mainPanel.add(tablePanel, BorderLayout.CENTER);
// Create summary panel
JPanel summaryPanel = createSummaryPanel();
mainPanel.add(summaryPanel, BorderLayout.SOUTH);
add(mainPanel);
}
private JPanel createHeaderPanel() {
JPanel headerPanel = new JPanel(new BorderLayout());
// Title
JLabel titleLabel = new JLabel("Personal Expense Tracker", JLabel.CENTER);
titleLabel.setFont(new Font("Arial", Font.BOLD, 24));
titleLabel.setForeground(new Color(44, 62, 80));
headerPanel.add(titleLabel, BorderLayout.NORTH);
// Toolbar
JPanel toolbarPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addButton = new JButton("Add Expense");
addButton.addActionListener(this::showAddExpenseDialog);
JButton editButton = new JButton("Edit Expense");
editButton.addActionListener(this::editSelectedExpense);
JButton deleteButton = new JButton("Delete Expense");
deleteButton.addActionListener(this::deleteSelectedExpense);
JButton categoriesButton = new JButton("Manage Categories");
categoriesButton.addActionListener(this::showCategoriesDialog);
JButton chartsButton = new JButton("View Charts");
chartsButton.addActionListener(this::showChartsDialog);
toolbarPanel.add(addButton);
toolbarPanel.add(editButton);
toolbarPanel.add(deleteButton);
toolbarPanel.add(categoriesButton);
toolbarPanel.add(chartsButton);
// Filter panel
JPanel filterPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
filterPanel.add(new JLabel("Filter by:"));
monthComboBox = new JComboBox<>(new String[]{
"All Months", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
});
monthComboBox.addActionListener(e -> filterExpenses());
yearComboBox = new JComboBox<>();
yearComboBox.addActionListener(e -> filterExpenses());
JButton clearFilterButton = new JButton("Clear Filter");
clearFilterButton.addActionListener(e -> clearFilters());
filterPanel.add(monthComboBox);
filterPanel.add(yearComboBox);
filterPanel.add(clearFilterButton);
JPanel combinedToolbar = new JPanel(new BorderLayout());
combinedToolbar.add(toolbarPanel, BorderLayout.NORTH);
combinedToolbar.add(filterPanel, BorderLayout.SOUTH);
headerPanel.add(combinedToolbar, BorderLayout.CENTER);
return headerPanel;
}
private JPanel createTablePanel() {
JPanel tablePanel = new JPanel(new BorderLayout());
// Create table model
String[] columnNames = {
"ID", "Date", "Description", "Amount", "Category", "Payment Method", "Notes"
};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // Make table non-editable
}
};
expensesTable = new JTable(tableModel);
expensesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
expensesTable.getColumnModel().getColumn(0).setMaxWidth(50); // ID column
expensesTable.getColumnModel().getColumn(1).setMaxWidth(100); // Date column
expensesTable.getColumnModel().getColumn(3).setMaxWidth(100); // Amount column
expensesTable.getColumnModel().getColumn(4).setMaxWidth(120); // Category column
expensesTable.getColumnModel().getColumn(5).setMaxWidth(120); // Payment Method column
JScrollPane scrollPane = new JScrollPane(expensesTable);
tablePanel.add(scrollPane, BorderLayout.CENTER);
return tablePanel;
}
private JPanel createSummaryPanel() {
JPanel summaryPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
summaryPanel.setBorder(BorderFactory.createTitledBorder("Summary"));
totalLabel = new JLabel("Total Expenses: $0.00");
totalLabel.setFont(new Font("Arial", Font.BOLD, 16));
totalLabel.setForeground(new Color(231, 76, 60));
summaryPanel.add(totalLabel);
return summaryPanel;
}
private void loadExpenses() {
loadExpenses(LocalDate.of(1970, 1, 1), LocalDate.now());
}
private void loadExpenses(LocalDate startDate, LocalDate endDate) {
tableModel.setRowCount(0);
List<Expense> expenses = expenseService.getExpensesByDateRange(startDate, endDate);
for (Expense expense : expenses) {
Object[] row = {
expense.getId(),
expense.getDate(),
expense.getDescription(),
String.format("$%.2f", expense.getAmount()),
expense.getCategory().getName(),
expense.getPaymentMethod().getDisplayName(),
expense.getNotes() != null ? expense.getNotes() : ""
};
tableModel.addRow(row);
}
updateTotalLabel(startDate, endDate);
}
private void updateTotalLabel(LocalDate startDate, LocalDate endDate) {
BigDecimal total = expenseService.getTotalExpenses(startDate, endDate);
totalLabel.setText(String.format("Total Expenses: $%.2f", total));
}
private void loadFilterData() {
// Populate years (current year and 5 years back)
int currentYear = LocalDate.now().getYear();
yearComboBox.removeAllItems();
yearComboBox.addItem(0); // 0 represents all years
for (int year = currentYear; year >= currentYear - 5; year--) {
yearComboBox.addItem(year);
}
}
private void filterExpenses() {
int selectedMonth = monthComboBox.getSelectedIndex(); // 0 = All Months
int selectedYear = (Integer) yearComboBox.getSelectedItem();
LocalDate startDate, endDate;
if (selectedYear == 0) {
// All years
startDate = LocalDate.of(1970, 1, 1);
endDate = LocalDate.now();
} else if (selectedMonth == 0) {
// All months of selected year
startDate = LocalDate.of(selectedYear, 1, 1);
endDate = LocalDate.of(selectedYear, 12, 31);
} else {
// Specific month and year
YearMonth yearMonth = YearMonth.of(selectedYear, selectedMonth);
startDate = yearMonth.atDay(1);
endDate = yearMonth.atEndOfMonth();
}
loadExpenses(startDate, endDate);
}
private void clearFilters() {
monthComboBox.setSelectedIndex(0);
yearComboBox.setSelectedIndex(0);
loadExpenses();
}
private void showAddExpenseDialog(ActionEvent e) {
AddExpenseDialog dialog = new AddExpenseDialog(this, expenseService);
dialog.setVisible(true);
if (dialog.isSuccess()) {
loadExpenses();
}
}
private void editSelectedExpense(ActionEvent e) {
int selectedRow = expensesTable.getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(this, "Please select an expense to edit.", 
"No Selection", JOptionPane.WARNING_MESSAGE);
return;
}
Integer expenseId = (Integer) tableModel.getValueAt(selectedRow, 0);
List<Expense> expenses = expenseService.getAllExpenses();
Expense selectedExpense = expenses.stream()
.filter(exp -> exp.getId().equals(expenseId))
.findFirst()
.orElse(null);
if (selectedExpense != null) {
AddExpenseDialog dialog = new AddExpenseDialog(this, expenseService, selectedExpense);
dialog.setVisible(true);
if (dialog.isSuccess()) {
loadExpenses();
}
}
}
private void deleteSelectedExpense(ActionEvent e) {
int selectedRow = expensesTable.getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(this, "Please select an expense to delete.", 
"No Selection", JOptionPane.WARNING_MESSAGE);
return;
}
Integer expenseId = (Integer) tableModel.getValueAt(selectedRow, 0);
String description = (String) tableModel.getValueAt(selectedRow, 2);
int confirm = JOptionPane.showConfirmDialog(this,
"Are you sure you want to delete expense: " + description + "?",
"Confirm Delete", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
boolean success = expenseService.deleteExpense(expenseId);
if (success) {
JOptionPane.showMessageDialog(this, "Expense deleted successfully.");
loadExpenses();
} else {
JOptionPane.showMessageDialog(this, "Failed to delete expense.", 
"Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void showCategoriesDialog(ActionEvent e) {
CategoriesDialog dialog = new CategoriesDialog(this, expenseService);
dialog.setVisible(true);
loadExpenses(); // Refresh in case categories were changed
}
private void showChartsDialog(ActionEvent e) {
ChartsDialog dialog = new ChartsDialog(this, expenseService);
dialog.setVisible(true);
}
public static void main(String[] args) {
// Set system look and feel
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());
} catch (Exception e) {
e.printStackTrace();
}
// Initialize database
DatabaseConnection.initializeDatabase();
SwingUtilities.invokeLater(() -> {
new ExpenseTrackerApp().setVisible(true);
});
}
}

Dialog Windows

package com.expensetracker.gui;
import com.expensetracker.models.Expense;
import com.expensetracker.models.Category;
import com.expensetracker.services.ExpenseService;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
public class AddExpenseDialog extends JDialog {
private final ExpenseService expenseService;
private Expense expense;
private boolean success = false;
private JTextField amountField;
private JTextField descriptionField;
private JComboBox<Category> categoryComboBox;
private JComboBox<Expense.PaymentMethod> paymentMethodComboBox;
private JTextArea notesArea;
private JSpinner dateSpinner;
public AddExpenseDialog(Frame owner, ExpenseService expenseService) {
this(owner, expenseService, null);
}
public AddExpenseDialog(Frame owner, ExpenseService expenseService, Expense expense) {
super(owner, expense == null ? "Add New Expense" : "Edit Expense", true);
this.expenseService = expenseService;
this.expense = expense;
initializeUI();
if (expense != null) {
populateFields();
}
}
private void initializeUI() {
setSize(400, 400);
setLocationRelativeTo(getOwner());
setResizable(false);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Form panel
JPanel formPanel = new JPanel(new GridLayout(6, 2, 5, 5));
// Amount
formPanel.add(new JLabel("Amount:"));
amountField = new JTextField();
formPanel.add(amountField);
// Description
formPanel.add(new JLabel("Description:"));
descriptionField = new JTextField();
formPanel.add(descriptionField);
// Category
formPanel.add(new JLabel("Category:"));
categoryComboBox = new JComboBox<>();
loadCategories();
formPanel.add(categoryComboBox);
// Payment Method
formPanel.add(new JLabel("Payment Method:"));
paymentMethodComboBox = new JComboBox<>(Expense.PaymentMethod.values());
formPanel.add(paymentMethodComboBox);
// Date
formPanel.add(new JLabel("Date:"));
SpinnerDateModel dateModel = new SpinnerDateModel();
dateSpinner = new JSpinner(dateModel);
JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(dateSpinner, "yyyy-MM-dd");
dateSpinner.setEditor(dateEditor);
formPanel.add(dateSpinner);
// Notes
formPanel.add(new JLabel("Notes:"));
notesArea = new JTextArea(3, 20);
JScrollPane notesScrollPane = new JScrollPane(notesArea);
formPanel.add(notesScrollPane);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons panel
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton saveButton = new JButton("Save");
JButton cancelButton = new JButton("Cancel");
saveButton.addActionListener(this::saveExpense);
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(saveButton);
buttonPanel.add(cancelButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
add(mainPanel);
}
private void loadCategories() {
List<Category> categories = expenseService.getAllCategories();
for (Category category : categories) {
categoryComboBox.addItem(category);
}
}
private void populateFields() {
if (expense != null) {
amountField.setText(expense.getAmount().toString());
descriptionField.setText(expense.getDescription());
// Select the category in combo box
for (int i = 0; i < categoryComboBox.getItemCount(); i++) {
if (categoryComboBox.getItemAt(i).equals(expense.getCategory())) {
categoryComboBox.setSelectedIndex(i);
break;
}
}
paymentMethodComboBox.setSelectedItem(expense.getPaymentMethod());
dateSpinner.setValue(java.sql.Date.valueOf(expense.getDate()));
notesArea.setText(expense.getNotes() != null ? expense.getNotes() : "");
}
}
private void saveExpense(ActionEvent e) {
try {
// Validate inputs
BigDecimal amount = new BigDecimal(amountField.getText().trim());
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
String description = descriptionField.getText().trim();
if (description.isEmpty()) {
throw new IllegalArgumentException("Description is required");
}
Category selectedCategory = (Category) categoryComboBox.getSelectedItem();
if (selectedCategory == null) {
throw new IllegalArgumentException("Please select a category");
}
Expense.PaymentMethod paymentMethod = (Expense.PaymentMethod) paymentMethodComboBox.getSelectedItem();
java.util.Date selectedDate = (java.util.Date) dateSpinner.getValue();
LocalDate date = selectedDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
String notes = notesArea.getText().trim();
if (expense == null) {
// Create new expense
Expense newExpense = new Expense(amount, description, selectedCategory, date, paymentMethod, notes);
expenseService.addExpense(newExpense);
} else {
// Update existing expense
expense.setAmount(amount);
expense.setDescription(description);
expense.setCategory(selectedCategory);
expense.setDate(date);
expense.setPaymentMethod(paymentMethod);
expense.setNotes(notes);
expenseService.updateExpense(expense);
}
success = true;
dispose();
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(this, "Please enter a valid amount.", 
"Invalid Amount", JOptionPane.ERROR_MESSAGE);
} catch (IllegalArgumentException ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), 
"Validation Error", JOptionPane.ERROR_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Error saving expense: " + ex.getMessage(), 
"Error", JOptionPane.ERROR_MESSAGE);
}
}
public boolean isSuccess() {
return success;
}
}

Charts and Reports

package com.expensetracker.gui;
import com.expensetracker.models.Category;
import com.expensetracker.services.ExpenseService;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.data.general.DefaultPieDataset;
import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Map;
public class ChartsDialog extends JDialog {
private final ExpenseService expenseService;
public ChartsDialog(Frame owner, ExpenseService expenseService) {
super(owner, "Expense Charts", true);
this.expenseService = expenseService;
initializeUI();
}
private void initializeUI() {
setSize(800, 600);
setLocationRelativeTo(getOwner());
JTabbedPane tabbedPane = new JTabbedPane();
// Current month chart
tabbedPane.addTab("Current Month", createCurrentMonthChart());
// All time chart
tabbedPane.addTab("All Time", createAllTimeChart());
add(tabbedPane);
}
private JPanel createCurrentMonthChart() {
YearMonth currentMonth = YearMonth.now();
LocalDate startDate = currentMonth.atDay(1);
LocalDate endDate = currentMonth.atEndOfMonth();
Map<Category, BigDecimal> expensesByCategory = 
expenseService.getExpensesByCategory(startDate, endDate);
DefaultPieDataset dataset = new DefaultPieDataset();
for (Map.Entry<Category, BigDecimal> entry : expensesByCategory.entrySet()) {
dataset.setValue(entry.getKey().getName(), entry.getValue());
}
JFreeChart chart = ChartFactory.createPieChart(
"Expenses for " + currentMonth.getMonth() + " " + currentMonth.getYear(),
dataset,
true, // include legend
true,
false
);
return new ChartPanel(chart);
}
private JPanel createAllTimeChart() {
Map<Category, BigDecimal> expensesByCategory = 
expenseService.getExpensesByCategory(
LocalDate.of(1970, 1, 1), 
LocalDate.now()
);
DefaultPieDataset dataset = new DefaultPieDataset();
for (Map.Entry<Category, BigDecimal> entry : expensesByCategory.entrySet()) {
dataset.setValue(entry.getKey().getName(), entry.getValue());
}
JFreeChart chart = ChartFactory.createPieChart(
"All Time Expenses by Category",
dataset,
true,
true,
false
);
return new ChartPanel(chart);
}
}

Key Features

Core Functionality

  1. Expense Management - Add, edit, delete, and view expenses
  2. Category Management - Organize expenses by categories
  3. Date Filtering - Filter expenses by month and year
  4. Payment Methods - Track different payment methods

Data Visualization

  1. Pie Charts - Visualize expenses by category
  2. Summary Statistics - Total expenses and category breakdowns
  3. Tabular Display - Clean, sortable expense table

Data Persistence

  1. SQLite Database - Lightweight, file-based database
  2. Automatic Backup - Database file can be easily backed up
  3. Data Integrity - Foreign key constraints and validation

User Experience

  1. Intuitive UI - Clean, modern interface
  2. Form Validation - Comprehensive input validation
  3. Error Handling - User-friendly error messages
  4. Responsive Design - Adapts to different screen sizes

Usage Instructions

  1. Adding Expenses: Click "Add Expense" and fill in the details
  2. Viewing Expenses: Use the table to browse all expenses
  3. Filtering: Use month/year dropdowns to filter expenses
  4. Charts: Click "View Charts" to see visual breakdowns
  5. Categories: Manage expense categories through "Manage Categories"

Building and Running

# Compile and run with Maven
mvn clean compile
mvn exec:java -Dexec.mainClass="com.expensetracker.ExpenseTrackerApp"
# Or create a JAR file
mvn clean package
java -jar target/expense-tracker-1.0.0.jar

This expense tracker provides a complete desktop solution for personal finance management with a professional-grade user interface and robust data management capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper