How I Built a Student Course Registration System to Learn Java OOP
When I was learning Java, I reached a point where simply knowing how to create classes and objects was no longer enough. I wanted to understand how the different concepts of object-oriented programming fit together when building an actual application.
At the time, two areas I was particularly interested in were abstraction and exception handling. Abstraction was especially confusing for me. I understood the syntax of an abstract class and an abstract method, but I was still asking myself a basic question: Why do I actually need abstraction?
Instead of trying to understand the concept only through small examples, I decided to build a project around it.
That project became a Student Course Registration System, a console-based Java application that allows students to be registered, courses to be assigned to students, and administrators to access student and course information.
The application is relatively small, but it gave me a practical environment to work with inheritance, encapsulation, abstraction, polymorphism, and exception handling together.
More importantly, it helped me understand that knowing the syntax of an OOP concept and knowing when to use it are two different things.
Why I Built This Project
I built this project while I was learning Java OOP.
My main goal was to stop treating OOP concepts as separate topics. I wanted to see what would happen when I had to use several of them in the same application.
For example, I could create a simple example of inheritance and understand that one class can extend another. I could create an abstract class and understand how abstract methods work. But those examples did not always answer the more practical question of why I would design an application that way.
The Student Course Registration System gave me an opportunity to experiment with that.
I wanted students to have their own information and enrollment behavior. I also wanted administrators to have their own authentication behavior. That naturally gave me different types of objects and relationships to work with.
I was especially interested in abstraction because it was the concept I was having the most difficulty understanding.
I knew what an abstract class looked like in Java, but I didn't initially understand why I would create a class that could not be instantiated directly.
Building this project helped me start answering that question.
The Application Structure
The application is a console program with two main areas: a student portal and an admin portal.
The student side handles operations such as registering students and enrolling them in courses. The admin side provides authentication and access to student and course information.
The application's main menu is handled by the Main class.
SystemManager manager = new SystemManager();
boolean run = true;
while(run) {
// display menu
// read selection
switch (select) {
case 1:
manager.enrollStudent();
break;
case 2:
manager.addCourse();
break;
// ...
}
}
I created a separate SystemManager rather than putting all of the application logic inside Main.
The main relationships between the classes can be simplified like this:
Main
|
v
SystemManager
|
+---- Student[]
|
+---- AdminSystem
|
+---- Course
User (abstract)
|
+---- Student
Admin (abstract)
|
+---- AdminSystem
This was not intended to be a large enterprise architecture. I was mainly trying to learn how to divide responsibilities between objects.
Dividing Responsibilities Between Classes
The project contains several classes, and each one has a particular responsibility.
Main is responsible primarily for starting the application, displaying the menu, and receiving the user's selections.
SystemManager coordinates operations such as registering students, searching for students, displaying information, and enrolling students in courses.
Student represents a student and contains student-specific information and behavior.
User contains information common to users and defines an abstract displayDetails() method.
Admin provides the structure for an administrator, while AdminSystem implements the administrator's login behavior.
Course represents the course-related part of the application.
This separation was important to me because I was trying to move away from writing one large class that handled everything.
I wanted the classes to have responsibilities of their own.
Encapsulation: Keeping Data and Behavior Together
The Student class was one of the clearest places where I practiced encapsulation.
Some of the common student information comes from the User class:
protected String name;
protected String fathername;
protected int rollno;
protected int age;
The Student class provides getters and setters for accessing and modifying this information:
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge(){
return age;
}
The student's courses are stored inside the Student object:
private String[] courses = new String[2];
int coursecount = 0;
The courses array is private, so other parts of the application don't directly modify it.
Instead, the Student class provides an enrollCourse() method:
public boolean enrollCourse(String course){
if(coursecount == 2){
return false;
}
else {
courses[coursecount] = course;
coursecount++;
return true;
}
}
This was a useful example for me because it showed that encapsulation is not simply about making variables private.
The object can also contain the behavior that operates on its data.
In this case, the Student object is responsible for managing its own course enrollment and enforcing the two-course limit.
Using Inheritance
I used inheritance in two parts of the application.
The first relationship is between User and Student:
abstract class User {
// common user information
}
public class Student extends User {
// student-specific information
}
The second is between Admin and AdminSystem:
public abstract class Admin {
// administrator structure
}
public class AdminSystem extends Admin {
// administrator implementation
}
The User class contains information that can be shared by different types of users:
protected String name;
protected String fathername;
protected int rollno;
protected int age;
Instead of repeating those fields in every user-related class, they can be defined in the parent class.
Student can then add behavior that is specific to students.
This was where inheritance started to make more sense to me.
Previously, I mostly thought of inheritance as a way to reuse code. Working on the project helped me see that it can also represent a relationship between classes.
A student is a type of user, so having Student extend User made sense within the design I was experimenting with.
The OOP Concept I Struggled With Most: Abstraction
Abstraction was probably the most interesting part of this project for me because it was the concept I was struggling to understand before I started.
I knew how to write an abstract class, but knowing the syntax wasn't enough.
My User class contains common user information and an abstract method:
abstract class User {
protected String name;
protected String fathername;
protected int rollno;
protected int age;
public abstract void displayDetails();
}
The important part here is that User does not provide the implementation of displayDetails().
The subclass provides it.
For example, Student implements the method:
public void displayDetails(){
System.out.println("Student Name : " + getName());
System.out.println("Father Name : " + getFatherName());
System.out.println("Age : " + getAge());
System.out.println("Roll No :" + getRollno());
// display enrolled courses
}
I used a similar idea for the administrator side:
public abstract class Admin {
protected String username;
protected int pin;
public abstract boolean login(String username, int pin);
}
AdminSystem then provides the implementation:
@Override
public boolean login(String user, int pass) {
if(username.equals(user) && pin == pass){
return true;
}
else {
return false;
}
}
This was the point where abstraction became more practical for me.
Before building the project, I was mostly thinking about abstraction as a Java feature. After using it, I started to understand it as a way of defining common structure while leaving certain behavior for subclasses to implement.
I wouldn't say this project made me an expert in abstraction, but it changed the way I thought about it.
Instead of asking only "How do I create an abstract class?", I started asking "What should be common, and what should be implemented differently by each subclass?"
That was a much more useful question.
Seeing Polymorphism in the Project
Polymorphism was another concept I practiced through the abstract classes.
User declares:
public abstract void displayDetails();
while Student provides the implementation.
Similarly, Admin declares:
public abstract boolean login(String username, int pin);
and AdminSystem overrides the method.
The @Override annotation makes that relationship clear:
@Override
public boolean login(String user, int pass) {
// implementation
}
What helped me was seeing that the parent class can define a behavior that subclasses are expected to provide, while each subclass can implement that behavior according to its own needs.
This made polymorphism easier to understand than when I was learning it only from definitions.
Handling Invalid Input With Exceptions
Exception handling was another important part of the project.
Because the application is console-based, it receives a lot of input from the user. Some of that input needs to be converted from strings into integers.
For example:
int select = Integer.parseInt(scan.nextLine());
If a user enters something that cannot be converted into an integer, a NumberFormatException can occur.
I handle this with a try-catch block:
try {
int select = Integer.parseInt(scan.nextLine());
// menu processing
}
catch (NumberFormatException e){
System.out.println("Selection cannot be non-numeric");
}
I also use exception handling when registering a student:
try {
String name = scan.nextLine();
String fathername = scan.nextLine();
int age = Integer.parseInt(scan.nextLine());
int rollno = Integer.parseInt(scan.nextLine());
// register student
}
catch (NumberFormatException e){
System.out.println(
"---- (Age - rollno ) cannot be non numeric ----"
);
}
catch (Exception e){
System.out.println(
"---- Unexpected error occurred ----"
);
}
This was useful because exception handling was no longer just something I had to memorize for an exam.
I could see exactly why it was necessary.
A console application depends on user input, and users don't always enter what the program expects. Handling the exception allows the program to respond with a message instead of immediately terminating.
How the Objects Communicate
The different classes work together through their objects.
Main creates a SystemManager:
SystemManager manager = new SystemManager();
The manager maintains the student collection:
Student[] student = new Student[100];
int studentcount = 0;
When a student is registered, the manager creates a Student object and assigns its information:
student[studentcount] = new Student();
student[studentcount].setName(name);
student[studentcount].setRollno(rollno);
student[studentcount].setAge(age);
student[studentcount].setFatherName(fathername);
studentcount++;
When a course needs to be assigned, the manager finds the relevant student and calls the student's enrollment method:
boolean enrolled = s.enrollCourse(c);
The Student object then handles the enrollment itself:
if(coursecount == 2){
return false;
}
I liked this part of the design because the manager coordinates the operation, while the Student object handles behavior that belongs specifically to the student.
The administrator side works in a similar way.
The application creates an AdminSystem object:
AdminSystem admin = new AdminSystem();
and delegates authentication to it:
boolean Checklogin = admin.login(name, pin);
Working with these relationships helped me understand that OOP is not only about creating classes. The more important part is deciding how those objects should interact.
The Challenges I Faced
The biggest challenge wasn't writing individual Java statements. It was trying to design the application using OOP concepts while I was still learning what good OOP design actually meant.
Abstraction was the clearest example.
I knew that I could create an abstract class, but I didn't initially understand why I should.
Building User and Admin as abstract classes forced me to think about what should be common and what should be implemented by subclasses.
I also had to think about where particular behavior should live.
For example, course enrollment is performed through:
s.enrollCourse(c);
The operation belongs to the Student class rather than being implemented entirely inside SystemManager.
That decision helped me understand the idea of giving an object responsibility for behavior related to its own data.
I also learned that getting a program to work and designing it well are not always the same thing.
The project works as a learning application, but there are several parts I would design differently if I built it again.
What I Would Improve
The current project uses fixed-size arrays:
Student[] student = new Student[100];
and the student has a fixed course capacity:
private String[] courses = new String[2];
These choices were enough for the project I was building at the time, but they are restrictive.
If I rebuilt the application, I would consider using ArrayList for the students and courses.
That would make the application more flexible and remove the fixed limits.
I would also revisit the Course design.
The current implementation uses static arrays for course names and codes. If I were redesigning the application today, I would think more carefully about representing individual courses as objects and how those objects should relate to students.
The input validation could also be improved so that invalid input is handled consistently throughout the application.
Most importantly, I would spend more time refining the class responsibilities.
That is one of the things I understand better now than when I first built the project. OOP design isn't just about using inheritance, abstract classes, and getters and setters because they are available in Java. The real challenge is deciding whether those tools actually make the application easier to understand and maintain.
What I Learned From the Project
The biggest lesson from this project was that knowing the definition of an OOP concept is different from knowing how to use it.
I could read that encapsulation means bundling data and behavior, that inheritance allows one class to inherit from another, or that abstraction hides implementation details.
But implementing those concepts in a real application forced me to think about them differently.
Abstraction was particularly valuable because it was the concept I found confusing at the beginning.
After working with User and Admin as abstract classes, I started to understand why a parent class might define common structure while leaving specific behavior to subclasses.
I also became more comfortable with exception handling because I could see a real reason for catching exceptions instead of simply learning the syntax of try and catch.
Another important lesson was about application design.
When you build a small program, it is tempting to put everything in one place because that can be faster initially. But as soon as the application contains several different responsibilities, separating those responsibilities becomes much more useful.
This project helped me start thinking in terms of objects, responsibilities, and relationships instead of only thinking about individual lines of code.
Conclusion
I built the Student Course Registration System while I was learning Java OOP because I wanted to practice the concepts in an actual application rather than learning them only through small examples.
The project gave me a practical way to work with inheritance, encapsulation, abstraction, polymorphism, and exception handling.
The most important part for me was abstraction.
Initially, I understood the syntax of abstract classes, but I wasn't sure why I needed them. Building the User and Admin classes helped me understand abstraction as a design concept: common structure can be defined in a parent class while specific behavior is left for subclasses.
The project also showed me that OOP design takes practice. Some of the decisions I made were appropriate for a small learning project, while others are areas I would change today, such as replacing fixed-size arrays with ArrayList and reconsidering how courses are represented.
I don't consider that a weakness of the project. For me, it is part of the learning process.
Building something once gives me the opportunity to look back at the code later and understand what I would do differently.
That is one of the main reasons I enjoy building projects while learning a technology: the code becomes more than an exercise. It becomes a record of how my understanding developed.
Project Repository
The complete source code for this project is available on GitHub:
Student Course Registration System — Java OOP
https://github.com/Hamadullah-Odho/StudentCourseRegistrationSystem-JAVA-OOP
Top comments (0)