A course registration system is a practical first Java project because it forces you to model real things, keep each thing’s data with the behavior that changes it, and enforce rules such as capacity limits and duplicate enrollments. This guide walks through a reference design with three core classes, one interface, and a small entry point. It is a design you can follow and adapt. It is not a report on any particular codebase, so the Java concepts below are tied to Oracle’s official documentation rather than to measured outcomes.
What the system needs to do
Keep the first version small. A workable scope for a learning project is:
- Register a student with an ID and a name.
- Create a course with a code, a title, and a maximum capacity.
- Enroll a registered student in an existing course.
- Reject enrollment when the course is full, the student is unknown, the course does not exist, or the student is already enrolled.
- List the courses that are available.
Prerequisites, waitlists, time-conflict checks, drop operations, and persistence to a file or database are real registration features, but each adds a separate design question. Leave them for later versions.
The OOP ideas the project exercises
Oracle’s tutorial lesson Object-Oriented Programming Concepts defines the two foundations in plain terms. A class is “a blueprint or prototype from which objects are created,” and an object is “a software bundle of related state and behavior.” In a registration system, a Student object holds an ID and a name, and it is the class that says what every student object looks like.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Classes and objects
Give each class fields for state that must persist across method calls, and methods for behavior that acts on that state. Keep fields private and expose only what other classes need. This is encapsulation, and it matters here because the capacity rule must not be bypassed by code that edits a course’s roster directly.
Inheritance
Oracle’s tutorial presents inheritance as a way to organize software through superclass and subclass relationships. In Java a class has exactly one direct superclass, so inheritance is a single-chain tool. Use it only when subclasses genuinely share behavior. If you have only Student and nothing else that shares its structure, do not add a superclass just to demonstrate the feature. A defensible case appears only when you also model something like an instructor, and both share a Person base with an ID and a name.
Interfaces
The tutorial describes an interface as a contract that a class promises to implement. Unlike a class, an interface can be implemented by many unrelated classes, and a class can implement several interfaces. The Java Language Specification states that classes support single inheritance while interfaces may extend multiple interfaces, so do not describe a Java class as having multiple inheritance. This project uses an interface for storage, described in the build steps below.
Packages
A package is a namespace for related classes and interfaces. Splitting the code into model, service, and app packages makes the dependency direction visible: model classes know nothing about the service layer, and the entry point wires everything together.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
Map the domain to classes
Before writing code, decide which class owns each piece of state and each rule. The reference design uses this split:
| Class or interface | Package | State it holds | Behavior it owns |
|---|---|---|---|
Student |
model | ID, name | Identity and display data only |
Course |
model | Code, title, capacity, roster of enrolled student IDs | Checks capacity and rejects duplicate enrollment |
CourseRepository (interface) |
service | None (a contract) | Find, list, and save courses |
InMemoryCourseRepository |
service | Map of course code to Course |
Implements the contract using memory only |
Registrar |
service | Map of student ID to Student, plus a reference to the repository |
Coordinates enrollment: checks that student and course exist, then delegates the rule to Course |
The key decision is that Course enforces its own capacity. If the Registrar checked the roster directly, every future caller would have to repeat that check correctly. Keeping the rule with the data it protects is the central object-oriented habit this project teaches.
Build the project step by step
-
Install a current JDK. The examples assume Java SE 21 or a later long-term-support release. Check the installed version with
java -versionandjavac -version, and confirm they match. -
Create the folder layout. The source path must mirror the package name:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchSpecial offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.Rank #3
registration/ src/ com/example/registration/model/ com/example/registration/service/ com/example/registration/app/ -
Write
Student. Save it assrc/com/example/registration/model/Student.java:package com.example.registration.model; 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; } } -
Write
Course. Save it in the samemodelfolder. This class owns the capacity rule:package com.example.registration.model; import java.util.LinkedHashSet; import java.util.Set; public class Course { private final String code; private final String title; private final int capacity; private final Set<String> enrolledStudentIds = new LinkedHashSet<>(); public Course(String code, String title, int capacity) { if (capacity < 1) { throw new IllegalArgumentException("Capacity must be at least 1"); } this.code = code; this.title = title; this.capacity = capacity; } public String getCode() { return code; } public String getTitle() { return title; } public boolean isFull() { return enrolledStudentIds.size() >= capacity; } public void addStudent(String studentId) { if (isFull()) { throw new IllegalStateException("Course " + code + " is full"); } if (!enrolledStudentIds.add(studentId)) { throw new IllegalStateException("Student " + studentId + " is already enrolled in " + code); } } }LinkedHashSetkeeps duplicate checks fast while preserving enrollment order, which makes printed rosters predictable. The Java SE 21 API documents the Collections Framework that these types belong to. -
Define the storage contract. Save this interface in
serviceasCourseRepository.java:Recommended Free Tools
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.package com.example.registration.service; import com.example.registration.model.Course; import java.util.List; import java.util.Optional; public interface CourseRepository { Optional<Course> findByCode(String code); List<Course> findAll(); void save(Course course); }Any class that implements this interface must provide all three methods. The
Registrardepends only on the contract, so a file-based or database version can replace the memory version without changing enrollment logic. -
Implement the interface in memory. Save
InMemoryCourseRepository.javainservice, using aHashMapkeyed by course code. Itssavemethod puts the course into the map, andfindAllreturns a new list of its values so callers cannot modify the map. -
Write
Registrar. Save it inservice:package com.example.registration.service; import com.example.registration.model.Course; import com.example.registration.model.Student; import java.util.HashMap; import java.util.Map; public class Registrar { private final CourseRepository courses; private final Map<String, Student> students = new HashMap<>(); public Registrar(CourseRepository courses) { this.courses = courses; } public void addStudent(Student student) { students.put(student.getId(), student); } public void enroll(String studentId, String courseCode) { if (!students.containsKey(studentId)) { throw new IllegalArgumentException("Unknown student " + studentId); } Course course = courses.findByCode(courseCode) .orElseThrow(() -> new IllegalArgumentException("Unknown course " + courseCode)); course.addStudent(studentId); courses.save(course); } }The final
savecall is redundant with the memory store, where the same object is already in the map. It is included because a database-backed repository would need it, and calling it keeps the service code correct for both. -
Write a short entry point. Save
Main.javainapp. Create the repository, register one or two courses and students, enroll a student, and print the result. Then deliberately enroll a second student into a course with capacity 1 and confirm that the exception message appears.Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.Best Value
-
Compile from the project root. On macOS or Linux:
javac -d out $(find src -name "*.java") java -cp out com.example.registration.app.MainOn Windows Command Prompt, run
dir /s /b src*.java > sources.txt, thenjavac -d out @sources.txt, thenjava -cp out com.example.registration.app.Main.
Check the rules before you extend the project
Test the rules with inputs you choose in advance. The expected behavior is:
- Enrolling a registered student in a course with free seats succeeds, and the course reports the same number of enrolled students you expected.
- Enrolling the same student twice in the same course throws
IllegalStateExceptionwith the duplicate message. - Enrolling a fourth student in a course with capacity 3 throws
IllegalStateExceptionwith the full message. - Enrolling an ID that was never registered throws
IllegalArgumentExceptionbefore the course is touched.
Write these checks as a main method or, once you are comfortable, as JUnit tests. Each check corresponds to one rule in the design, which makes failures easy to locate.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
class Course is public, should be declared in a file named Course.java |
The file name does not match the public class name | Rename the file so it matches the class exactly, including capitalization |
package com.example.registration.model does not exist |
The source folder layout does not match the package declaration | Move the file so its folder path matches its package line |
Error: Could not find or load main class |
The run command omits -cp out or uses the wrong fully qualified name |
Run java -cp out com.example.registration.app.Main from the project root |
| Registered courses disappear after the program exits | The in-memory repository keeps data only while the program runs | Expected behavior. Persistence requires a second implementation of CourseRepository |
| Enrollment fails with a full-course message on a course that looks empty | The same course object was not reused, or capacity was set lower than expected | Confirm the course stored in the repository is the instance you modified, and print its capacity when it is created |
Which Java learning material to follow
Oracle’s older Java tutorial, including the OOP lessons used as a reference here, uses examples written in the JDK 8 era. The language concepts are still accurate, but its code style and some library usage are dated. Oracle directs readers to Dev.java for updated material. Dev.java’s OOP learning section covers classes and packages, interfaces, records, and inheritance, which together match the topics in this project. Records are a natural next step: a read-only Enrollment record holding a student ID and course code could replace the roster set when you add enrollment history.
Where the reference design stops
This design does not handle concurrent registrations, a graphical or web interface, authentication, or real institutional rules such as prerequisites and time conflicts. Adding any of them changes the design, not just the code. Capacity and duplicate checks are the only validation rules implemented here, and they are enforced in memory during one run of the program.
Sources
- Oracle, Lesson: Object-Oriented Programming Concepts, The Java Tutorials (quoted definitions of class and object, and descriptions of inheritance, interfaces, and packages).
- Oracle, The Java Language Specification, Chapter 1 (language description and inheritance rules).
- Oracle, Java SE 21 API documentation, Java Collections Framework.
- Dev.java, OOP learning section (classes and packages, interfaces, records, inheritance).
The project code above is an illustrative design written for this guide. It has not been measured against any particular course or enrollment dataset.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




