Backend Systemsbeginner
Java Project: Grade Tracker App
Build a console-based student grade tracker in Java using OOP, collections, file I/O, and exceptions. A complete beginner project that ties everything together.
Asma HafeezApril 17, 20266 min read
javaprojectoopcollectionsfile-io
Java Project: Grade Tracker App
This project applies everything from the Java beginner series: OOP, generics, collections, streams, file I/O, and exception handling.
What We're Building
Grade Tracker
─────────────────────────
1. Add student
2. Add grade
3. List students
4. Show statistics
5. Save to file
6. Load from file
0. Exit
> 1
Enter student name: Alice
Student Alice added.
> 2
Enter student name: Alice
Enter subject: Math
Enter grade (0-100): 92
Grade added.
> 4
--- Statistics ---
Alice | Math: 92 | Avg: 92.0 | Top Grade: 92Project Structure
grade-tracker/
├── src/
│ ├── model/
│ │ ├── Student.java
│ │ └── GradeEntry.java
│ ├── service/
│ │ ├── GradeBook.java
│ │ └── FileStorage.java
│ └── Main.javaStep 1: Model Classes
JAVA
// src/model/GradeEntry.java
package model;
public record GradeEntry(String subject, double grade) {
public GradeEntry {
if (grade < 0 || grade > 100) {
throw new IllegalArgumentException("Grade must be between 0 and 100, got: " + grade);
}
if (subject == null || subject.isBlank()) {
throw new IllegalArgumentException("Subject cannot be blank");
}
}
public String letterGrade() {
if (grade >= 90) return "A";
if (grade >= 80) return "B";
if (grade >= 70) return "C";
if (grade >= 60) return "D";
return "F";
}
@Override
public String toString() {
return String.format("%s: %.1f (%s)", subject, grade, letterGrade());
}
}JAVA
// src/model/Student.java
package model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.OptionalDouble;
public class Student {
private final String name;
private final List<GradeEntry> grades;
public Student(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Student name cannot be blank");
}
this.name = name;
this.grades = new ArrayList<>();
}
public String getName() { return name; }
public List<GradeEntry> getGrades() {
return Collections.unmodifiableList(grades);
}
public void addGrade(GradeEntry entry) {
grades.add(entry);
}
public OptionalDouble average() {
return grades.stream()
.mapToDouble(GradeEntry::grade)
.average();
}
public OptionalDouble highestGrade() {
return grades.stream()
.mapToDouble(GradeEntry::grade)
.max();
}
public OptionalDouble lowestGrade() {
return grades.stream()
.mapToDouble(GradeEntry::grade)
.min();
}
public String summary() {
if (grades.isEmpty()) return name + " (no grades)";
String gradeList = grades.stream()
.map(GradeEntry::toString)
.reduce((a, b) -> a + ", " + b)
.orElse("");
return String.format("%-12s | %-40s | Avg: %.1f",
name, gradeList, average().orElse(0));
}
}Step 2: GradeBook Service
JAVA
// src/service/GradeBook.java
package service;
import model.GradeEntry;
import model.Student;
import java.util.*;
import java.util.stream.Collectors;
public class GradeBook {
private final Map<String, Student> students = new LinkedHashMap<>();
public void addStudent(String name) {
if (students.containsKey(name)) {
throw new IllegalArgumentException("Student already exists: " + name);
}
students.put(name, new Student(name));
}
public void addGrade(String studentName, String subject, double grade) {
Student student = getStudent(studentName);
student.addGrade(new GradeEntry(subject, grade));
}
public Student getStudent(String name) {
Student student = students.get(name);
if (student == null) {
throw new NoSuchElementException("Student not found: " + name);
}
return student;
}
public Collection<Student> getAllStudents() {
return Collections.unmodifiableCollection(students.values());
}
public List<Student> topStudents(int n) {
return students.values().stream()
.filter(s -> s.average().isPresent())
.sorted(Comparator.comparingDouble(s -> -s.average().orElse(0)))
.limit(n)
.collect(Collectors.toList());
}
public Map<String, Double> subjectAverages() {
Map<String, List<Double>> bySubject = new HashMap<>();
for (Student student : students.values()) {
for (GradeEntry entry : student.getGrades()) {
bySubject.computeIfAbsent(entry.subject(), k -> new ArrayList<>())
.add(entry.grade());
}
}
Map<String, Double> result = new LinkedHashMap<>();
bySubject.forEach((subject, grades) ->
result.put(subject, grades.stream().mapToDouble(Double::doubleValue).average().orElse(0))
);
return result;
}
public int studentCount() { return students.size(); }
public boolean isEmpty() { return students.isEmpty(); }
}Step 3: File Storage
JAVA
// src/service/FileStorage.java
package service;
import model.GradeEntry;
import model.Student;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class FileStorage {
private static final String HEADER = "student,subject,grade";
public void save(GradeBook book, Path path) throws IOException {
StringBuilder sb = new StringBuilder(HEADER).append("\n");
for (Student student : book.getAllStudents()) {
for (GradeEntry entry : student.getGrades()) {
sb.append(student.getName()).append(",")
.append(entry.subject()).append(",")
.append(entry.grade()).append("\n");
}
}
Files.writeString(path, sb.toString());
System.out.println("Saved to " + path);
}
public void load(GradeBook book, Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
int loaded = 0;
for (int i = 1; i < lines.size(); i++) { // skip header
String line = lines.get(i).trim();
if (line.isBlank()) continue;
String[] parts = line.split(",");
if (parts.length != 3) continue;
String studentName = parts[0].trim();
String subject = parts[1].trim();
double grade = Double.parseDouble(parts[2].trim());
if (!studentExists(book, studentName)) {
book.addStudent(studentName);
}
book.addGrade(studentName, subject, grade);
loaded++;
}
System.out.println("Loaded " + loaded + " grade(s) from " + path);
}
private boolean studentExists(GradeBook book, String name) {
try {
book.getStudent(name);
return true;
} catch (java.util.NoSuchElementException e) {
return false;
}
}
}Step 4: Main — Console UI
JAVA
// src/Main.java
import service.FileStorage;
import service.GradeBook;
import model.Student;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;
public class Main {
private static final GradeBook book = new GradeBook();
private static final FileStorage storage = new FileStorage();
private static final Scanner scanner = new Scanner(System.in);
private static final Path SAVE_FILE = Path.of("grades.csv");
public static void main(String[] args) {
System.out.println("=== Grade Tracker ===");
boolean running = true;
while (running) {
printMenu();
String choice = scanner.nextLine().trim();
running = handleChoice(choice);
}
System.out.println("Goodbye!");
scanner.close();
}
private static void printMenu() {
System.out.println("""
1. Add student
2. Add grade
3. List students
4. Show statistics
5. Save to file
6. Load from file
0. Exit
> """);
}
private static boolean handleChoice(String choice) {
switch (choice) {
case "1" -> addStudent();
case "2" -> addGrade();
case "3" -> listStudents();
case "4" -> showStatistics();
case "5" -> saveToFile();
case "6" -> loadFromFile();
case "0" -> { return false; }
default -> System.out.println("Invalid option. Try again.");
}
return true;
}
private static void addStudent() {
System.out.print("Enter student name: ");
String name = scanner.nextLine().trim();
try {
book.addStudent(name);
System.out.println("Student " + name + " added.");
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
private static void addGrade() {
System.out.print("Enter student name: ");
String name = scanner.nextLine().trim();
System.out.print("Enter subject: ");
String subject = scanner.nextLine().trim();
System.out.print("Enter grade (0-100): ");
String gradeStr = scanner.nextLine().trim();
try {
double grade = Double.parseDouble(gradeStr);
book.addGrade(name, subject, grade);
System.out.println("Grade added.");
} catch (NumberFormatException e) {
System.out.println("Invalid grade: must be a number.");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
private static void listStudents() {
if (book.isEmpty()) {
System.out.println("No students yet.");
return;
}
System.out.println("\n--- Students ---");
book.getAllStudents().forEach(s -> System.out.println(s.summary()));
}
private static void showStatistics() {
if (book.isEmpty()) {
System.out.println("No data yet.");
return;
}
System.out.println("\n--- Statistics ---");
System.out.println("\nTop 3 students:");
book.topStudents(3).forEach(s ->
System.out.printf(" %-12s Avg: %.1f%n", s.getName(), s.average().orElse(0))
);
System.out.println("\nSubject averages:");
book.subjectAverages().forEach((subject, avg) ->
System.out.printf(" %-15s %.1f%n", subject, avg)
);
}
private static void saveToFile() {
try {
storage.save(book, SAVE_FILE);
} catch (IOException e) {
System.out.println("Save failed: " + e.getMessage());
}
}
private static void loadFromFile() {
try {
storage.load(book, SAVE_FILE);
} catch (IOException e) {
System.out.println("Load failed: " + e.getMessage());
}
}
}Running the App
Bash
# Compile
javac -d out src/model/*.java src/service/*.java src/Main.java
# Run
java -cp out MainWhat This Project Demonstrates
- Records —
GradeEntryas an immutable value object - Encapsulation —
Studentcontrols its own grade list - Collections —
LinkedHashMapfor insertion-ordered students,ArrayListfor grades - Streams — averages, filtering, sorting with lambdas
- Custom exceptions — validated constructors with
IllegalArgumentException - File I/O — CSV save/load with
Files.writeString/Files.readAllLines - Switch expressions — clean menu dispatch
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.