Case Study 1: Designing a Student Information System

Context

GradeBook Pro has been growing steadily throughout this course. We have built input routines, computed averages, and worked with arrays of grades. But so far, student data has been scattered across parallel arrays — one for names, one for IDs, one for grades. It is time to redesign GradeBook Pro using records to create a proper Student Information System (SIS) that models students, courses, and enrollment as coherent data structures.

This case study walks through the design process from requirements to working code, illustrating how records transform a fragile, parallel-array program into a well-structured system.


The Requirements

Our Student Information System needs to track:

  1. Students — Each student has a name, a unique ID, an email address, a GPA, and an enrollment status.
  2. Courses — Each course has a code (like "CS101"), a title, a number of credits, and a maximum enrollment.
  3. Enrollments — A student can be enrolled in multiple courses, and each enrollment has an associated grade.
  4. Grade calculation — The system should compute a student's GPA from their course grades and credit hours.

Step 1: Identify the Entities

Before writing any code, we identify the core entities and their attributes. This is data modeling — deciding what record types we need and what fields each should contain.

Entity Attributes
Student Name, ID, Email, GPA, Enrolled (active/inactive), list of enrollments
Course Code, Title, Credits, MaxStudents, CurrentStudents
Enrollment CourseCode, Grade (A=4, B=3, C=2, D=1, F=0), Credits

Step 2: Define the Record Types

We work bottom-up, defining simpler types first since more complex types depend on them.

type
  TLetterGrade = (lgA, lgB, lgC, lgD, lgF, lgNone);

  TEnrollment = record
    CourseCode: string[10];
    Grade: TLetterGrade;
    Credits: Integer;
  end;

  TStudent = record
    Name: string;
    ID: Integer;
    Email: string;
    GPA: Real;
    Active: Boolean;
    Enrollments: array[1..8] of TEnrollment;
    NumEnrollments: Integer;
  end;

  TCourse = record
    Code: string[10];
    Title: string;
    Credits: Integer;
    MaxStudents: Integer;
    CurrentStudents: Integer;
  end;

Design decisions explained:

  • TLetterGrade is an enumerated type, not a string. This prevents typos and enables case statements.
  • TEnrollment is a separate record type. It could have been embedded directly in TStudent, but making it its own type enables us to write procedures that operate on single enrollments.
  • TStudent nests an array of TEnrollment records. Each student can enroll in up to 8 courses. NumEnrollments tracks how many slots are in use.
  • string[10] for course codes limits the field to 10 characters, saving memory compared to a full string (which is 255 characters by default in short-string mode).

Step 3: Core Operations

Computing GPA

GPA is a weighted average: sum of (grade points * credits) divided by total credits.

function GradePoints(G: TLetterGrade): Real;
begin
  case G of
    lgA: GradePoints := 4.0;
    lgB: GradePoints := 3.0;
    lgC: GradePoints := 2.0;
    lgD: GradePoints := 1.0;
    lgF: GradePoints := 0.0;
    lgNone: GradePoints := 0.0;
  end;
end;

procedure CalculateGPA(var Student: TStudent);
var
  I: Integer;
  TotalPoints, TotalCredits: Real;
begin
  TotalPoints := 0.0;
  TotalCredits := 0.0;
  for I := 1 to Student.NumEnrollments do
    if Student.Enrollments[I].Grade <> lgNone then
    begin
      TotalPoints := TotalPoints +
        GradePoints(Student.Enrollments[I].Grade) *
        Student.Enrollments[I].Credits;
      TotalCredits := TotalCredits + Student.Enrollments[I].Credits;
    end;
  if TotalCredits > 0 then
    Student.GPA := TotalPoints / TotalCredits
  else
    Student.GPA := 0.0;
end;

Notice that we pass the student by var because we are modifying the GPA field. Courses with grade lgNone (not yet graded) are excluded from the calculation.

Enrolling a Student in a Course

function EnrollStudent(var Student: TStudent; const Course: TCourse): Boolean;
begin
  EnrollStudent := False;
  if Student.NumEnrollments >= 8 then
  begin
    WriteLn('Error: Student is already enrolled in 8 courses.');
    Exit;
  end;
  if not Student.Active then
  begin
    WriteLn('Error: Student is not active.');
    Exit;
  end;
  Inc(Student.NumEnrollments);
  Student.Enrollments[Student.NumEnrollments].CourseCode := Course.Code;
  Student.Enrollments[Student.NumEnrollments].Grade := lgNone;
  Student.Enrollments[Student.NumEnrollments].Credits := Course.Credits;
  EnrollStudent := True;
end;

The function returns Boolean to indicate success or failure, allowing the caller to respond appropriately.

Assigning a Grade

procedure AssignGrade(var Student: TStudent;
                      const CourseCode: string;
                      NewGrade: TLetterGrade);
var
  I: Integer;
  Found: Boolean;
begin
  Found := False;
  for I := 1 to Student.NumEnrollments do
    if Student.Enrollments[I].CourseCode = CourseCode then
    begin
      Student.Enrollments[I].Grade := NewGrade;
      Found := True;
      Break;
    end;
  if not Found then
    WriteLn('Error: Student is not enrolled in ', CourseCode)
  else
    CalculateGPA(Student);  { Recalculate GPA after grade change }
end;

After assigning a grade, we immediately recalculate the student's GPA. This keeps the data consistent — the GPA is never stale.

Displaying a Student Transcript

procedure PrintTranscript(const Student: TStudent);
var
  I: Integer;
  GradeStr: string;
begin
  WriteLn('==========================================');
  WriteLn('STUDENT TRANSCRIPT');
  WriteLn('==========================================');
  WriteLn('Name:   ', Student.Name);
  WriteLn('ID:     ', Student.ID);
  WriteLn('Email:  ', Student.Email);
  WriteLn('Status: ', BoolToStr(Student.Active, 'Active', 'Inactive'));
  WriteLn('GPA:    ', Student.GPA:0:2);
  WriteLn;
  WriteLn('Course':10, 'Credits':10, 'Grade':10);
  WriteLn(StringOfChar('-', 30));

  for I := 1 to Student.NumEnrollments do
  begin
    case Student.Enrollments[I].Grade of
      lgA:    GradeStr := 'A';
      lgB:    GradeStr := 'B';
      lgC:    GradeStr := 'C';
      lgD:    GradeStr := 'D';
      lgF:    GradeStr := 'F';
      lgNone: GradeStr := '--';
    end;
    WriteLn(Student.Enrollments[I].CourseCode:10,
            Student.Enrollments[I].Credits:10,
            GradeStr:10);
  end;
  WriteLn('==========================================');
end;

This procedure uses const because it only reads the student data.


Step 4: Putting It Together

var
  Students: array[1..50] of TStudent;
  Courses: array[1..20] of TCourse;
  NumStudents, NumCourses: Integer;

A complete session might look like this:

{ Initialize a student }
Students[1].Name := 'Priya Sharma';
Students[1].ID := 20260001;
Students[1].Email := 'psharma@university.edu';
Students[1].Active := True;
Students[1].NumEnrollments := 0;

{ Set up courses }
Courses[1].Code := 'CS101';
Courses[1].Title := 'Intro to Computer Science';
Courses[1].Credits := 3;

Courses[2].Code := 'MATH201';
Courses[2].Title := 'Calculus II';
Courses[2].Credits := 4;

Courses[3].Code := 'ENG102';
Courses[3].Title := 'Composition';
Courses[3].Credits := 3;

{ Enroll and grade }
EnrollStudent(Students[1], Courses[1]);
EnrollStudent(Students[1], Courses[2]);
EnrollStudent(Students[1], Courses[3]);

AssignGrade(Students[1], 'CS101', lgA);
AssignGrade(Students[1], 'MATH201', lgB);
AssignGrade(Students[1], 'ENG102', lgA);

PrintTranscript(Students[1]);

Output:

==========================================
STUDENT TRANSCRIPT
==========================================
Name:   Priya Sharma
ID:     20260001
Email:  psharma@university.edu
Status: Active
GPA:    3.70
Course    Credits     Grade
------------------------------
     CS101         3         A
   MATH201         4         B
    ENG102         3         A
==========================================

The GPA is (43 + 34 + 4*3) / (3+4+3) = 36/10 = 3.60. Wait — let us recalculate: A in CS101 (4.0 * 3 = 12), B in MATH201 (3.0 * 4 = 12), A in ENG102 (4.0 * 3 = 12). Total points = 36, total credits = 10, GPA = 3.60. (The output would show 3.60, not 3.70 — this is a good reminder to always verify computed values.)


Lessons Learned

  1. Records make data modeling natural. Each entity in our domain (student, course, enrollment) maps to a record type. The code mirrors the problem.

  2. Nested records handle relationships. A student's enrollments are inside the student record, making it impossible for enrollment data to become separated from student data.

  3. Enumerated types improve reliability. Using TLetterGrade instead of strings eliminates typos and enables exhaustive case statements. The compiler warns if we forget a case.

  4. const parameters document intent. When PrintTranscript takes const Student: TStudent, readers immediately know the function is read-only.

  5. Keeping derived data consistent. GPA is derived from grades. By recalculating it whenever a grade changes, we ensure it is never stale. An alternative design would compute GPA on demand rather than storing it.


Your Turn

  1. Add a DropCourse procedure that removes an enrollment from a student (shift remaining enrollments down) and recalculates the GPA.
  2. Add a FindStudent function that searches the array by ID.
  3. Add a ClassList procedure that takes a course code and the students array, and prints all students enrolled in that course.
  4. Consider: should GPA be stored in the record, or computed on demand? What are the trade-offs?