Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A student course registration system is a good first Java project for learning object-oriented programming because its nouns map directly to classes: a student, a course, and a registration action that connects them. The shortest working version has three classes, one service that enforces the rules, and a small main program that prints results. This guide walks through that design step by step. It is a reference design, not a record of any particular author’s code, so the class names and rules below are choices you can change.

Start with the behaviour the system must have

Before writing any class, list what a user must be able to do. Keeping the list short stops the project from growing faster than your understanding of Java.

  • Add a student with a unique ID and a name.
  • Add a course with a code, a title, and a maximum number of seats.
  • Register a student in a course.
  • Reject a registration when the student or course does not exist.
  • Reject a duplicate registration.
  • Reject a registration when the course is full.

Anything beyond these rules, such as prerequisites, timetable clashes, saved data between runs, or a graphical screen, is a later extension. Adding them now would mostly teach you how to debug, not how objects work.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Map the nouns and verbs to classes

Oracle’s Java tutorial lesson on object-oriented programming describes a class as “a blueprint or prototype from which objects are created,” and an object as “a software bundle of related state and behavior.” In practice, that means each class should own the data it needs and the operations on that data. The table below shows a split that keeps each responsibility in one place.

Class State it holds Behaviour it provides OOP idea it practises
Student ID, name Read its ID and name Objects bundle state with accessor methods
Course Code, title, capacity, enrolled students Report whether it is full, check for a student, accept a student A class protects its own rules about its data
RegistrationService Lookup tables of students and courses Add students and courses, perform a registration with checks One object coordinates others without duplicating their logic

Keeping the capacity check inside Course and the existence checks inside RegistrationService is a design choice. Another reasonable layout puts all the logic in the service. The principle is to be consistent and to know where each rule lives.

Write the model classes

Student

A student needs an identifier that stays the same and a name. Both fields are final because nothing in this design changes them after creation.

package registration;

public class Student {
    private final String id;
    private final String name;

    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

Course

The course stores its own list of enrolled students and refuses an invalid capacity when it is created. Capacity checking lives here because the course is the object that knows its seat count.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package registration;

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

public class Course {
    private final String code;
    private final String title;
    private final int capacity;
    private final List<Student> enrolled = new ArrayList<>();

    public Course(String code, String title, int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("Capacity must be positive");
        }
        this.code = code;
        this.title = title;
        this.capacity = capacity;
    }

    public String getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    public boolean isFull() {
        return enrolled.size() >= capacity;
    }

    public boolean hasStudent(String studentId) {
        for (Student s : enrolled) {
            if (s.getId().equals(studentId)) {
                return true;
            }
        }
        return false;
    }

    public void addStudent(Student student) {
        enrolled.add(student);
    }
}

Notice that hasStudent compares IDs rather than relying on object identity. Two Student objects with the same ID describe the same person in this system, and comparing IDs makes that explicit. If you later override equals and hashCode in Student, you can simplify this method, but the ID comparison is clearer for a first version.

Put the enrollment logic in one service

RegistrationService

The service holds the lookup tables and runs the checks in a fixed order: student exists, course exists, no duplicate, seat available. The order matters because each later check assumes the earlier ones passed.

package registration;

import java.util.HashMap;
import java.util.Map;

public class RegistrationService {
    private final Map<String, Student> students = new HashMap<>();
    private final Map<String, Course> courses = new HashMap<>();

    public void addStudent(Student student) {
        students.put(student.getId(), student);
    }

    public void addCourse(Course course) {
        courses.put(course.getCode(), course);
    }

    public String register(String studentId, String courseCode) {
        Student student = students.get(studentId);
        if (student == null) {
            return "Unknown student: " + studentId;
        }
        Course course = courses.get(courseCode);
        if (course == null) {
            return "Unknown course: " + courseCode;
        }
        if (course.hasStudent(studentId)) {
            return "Already registered";
        }
        if (course.isFull()) {
            return "Course is full";
        }
        course.addStudent(student);
        return "Registered " + student.getName() + " in " + course.getCode();
    }
}

This method returns a text message, which keeps the first version easy to follow. The trade-off is that callers must parse text to know what happened. A stronger design returns a result type with a status field, or throws specific exceptions for each failure. Changing this later is a good exercise because it shows how a method’s return type is part of its contract.

Use an interface for a swappable rule

An interface describes a set of methods a class promises to provide. Oracle’s lesson describes an interface as a contract that a class promises to implement. In this project, a rule interface lets you add new enrollment rules without editing the service each time.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package registration;

public interface EnrollmentRule {
    // Returns null when the rule passes, or a message explaining the rejection.
    String check(Student student, Course course);
}

class CapacityRule implements EnrollmentRule {
    @Override
    public String check(Student student, Course course) {
        return course.isFull() ? "Course is full" : null;
    }
}

Returning null to mean “passed” is a simple convention that beginners often use. It works, but it is easy to forget the null check at the call site. Treat it as a learning shortcut and note where it could cause a NullPointerException.

When inheritance helps and when it does not

Java classes have a single superclass, while a class can implement any number of interfaces. The Java Language Specification describes Java as “a general-purpose, concurrent, class-based, object-oriented language” and states that classes support single inheritance, while interfaces can extend multiple interfaces. Plan for one superclass per class, and do not expect a class to inherit from two classes.

Inheritance is useful only when subtypes share behaviour and differ in a real way. If you want a GraduateStudent with a different credit limit, class GraduateStudent extends Student can express that. If every student behaves the same, a subclass adds complexity without teaching you anything useful about this system. Use the feature when the design needs it, not to demonstrate vocabulary.

Organise the project into packages

Oracle’s lesson describes a package as a namespace for organising related classes and interfaces. A folder layout that matches the package name keeps the project readable as it grows.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
src/
  registration/
    Student.java
    Course.java
    EnrollmentRule.java
    RegistrationService.java
    Main.java
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Compile and run from the command line

Use a JDK on your machine and run these steps from the folder that contains src.

  1. Create an output folder: mkdir out.
  2. Compile every source file into it: javac -d out src/registration/*.java.
  3. Run the main class by its fully qualified name: java -cp out registration.Main.

Add this Main class to src/registration/Main.java to exercise the rules:

package registration;

public class Main {
    public static void main(String[] args) {
        RegistrationService service = new RegistrationService();
        service.addStudent(new Student("S1", "Ada"));
        service.addCourse(new Course("CS101", "Intro to Programming", 1));

        System.out.println(service.register("S1", "CS101"));
        System.out.println(service.register("S1", "CS101"));
        System.out.println(service.register("S2", "CS101"));
    }
}

The expected output is:

Registered Ada in CS101
Already registered
Unknown student: S2

Change the capacity to 0 and the constructor throws an IllegalArgumentException, which is a useful way to see where invalid objects are stopped.

Common mistakes to check

  • Public fields. If Course exposes its list directly, outside code can bypass the capacity check.
  • Checks in the wrong order. Checking seats before checking duplicates can report “Course is full” to a student who is already enrolled.
  • Mismatched packages. If a file’s package line does not match its folder, javac and java commands will fail or find the wrong class.
  • Using the old tutorial examples as-is. See the version note below before copying examples.

Version and source notes

Oracle’s older Java tutorial examples are written for JDK 8-era code, and Oracle points readers to Dev.java for updated material. Dev.java has a newer object-oriented programming section covering classes and packages, interfaces, records, and inheritance. The collection types used in this guide, ArrayList and HashMap, come from the Java Collections Framework; the Java SE 21 API documentation describes the Collection interface that these types implement. The code above uses only standard library classes and does not require a third-party library.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Where to take the project next

  • Replace the text messages with a result type that carries a status and a message.
  • Add a prerequisite rule by implementing EnrollmentRule in a new class, without changing RegistrationService.
  • Save students and courses to a file so data survives between runs, and note which parts of the design change when you do.
  • Write a small set of checks for each rule so you can confirm a change did not break an earlier behaviour.

Each extension reuses the same three ideas: objects hold their own state, classes expose behaviour through methods, and interfaces let you swap a rule without rewriting the code around it.