Online Learning Platform
A course is a list of lessons, but "watched the video," "read the article," and "passed the
quiz" are three completely different definitions of done. The design question isn't how to
track progress - it's how to track it without Progress growing an if lesson is quiz for
every lesson type anyone invents next.
Requirements
Functional
- A course contains an ordered list of lessons; a lesson is a video, a text article, or a quiz.
- A user enrolls in a course.
- As the user works through lessons, each lesson's completion is tracked individually and rolls up into a course completion percentage.
- A quiz lesson only counts as complete once the user passes it with a minimum score; a video or text lesson completes when it's been opened and viewed.
Non-functional
- Adding a new lesson type must not require editing
Progressor the course percent-complete calculation - each lesson type decides its own definition of "done." - Looking up a user's progress on a course must not scan every enrollment in the system.
Design
Every lesson type has a different rule for "did the learner finish this," so that rule lives
on the lesson, not in whoever's asking. Progress never checks a lesson's type - it hands
the learner's raw interaction to Lesson.isComplete(...) and trusts the answer, so a new
lesson type is a new subclass, not a new branch somewhere else.
- 1Enrollment is created once, up front, before any lesson has been touched.
- 2Every interaction - a finished video, a quiz submission - comes through this one method.
- 3The platform doesn’t know if this is a video or a quiz; the lesson answers for itself.
- 4A true answer gets recorded per lesson - no course-wide recompute happens here.
- 5The course percentage is a fold over already-recorded progress, not a re-derivation from scratch.
Enrollment is the thing that actually outlives a single lesson: it's created once at
sign-up and accumulates one Progress entry per lesson as the learner moves through the
course, which is also what lets completion percentage be read cheaply instead of recomputed
by replaying every interaction.
Class diagram
Code
Design decisions
Lessonis an abstract class with one polymorphic method,isComplete.VideoLessonchecks a watched-percentage threshold,QuizLessonchecks a passing score,TextLessonchecks that it was opened.Progresscalls the same method on all three and never knows which one it's talking to - that's what makes lesson type an implementation detail instead of a fact every caller has to know.Progressstores one entry per (enrollment, lesson), not a single course-wide percentage. Recomputing a percentage from scratch on every read would mean walking every lesson every time; storing per-lesson completion means the course percentage is a cheap fold over data that's already there.Enrollmentis its own object rather than a field onUser. A user can enroll in many courses, and progress belongs to the pairing of user and course, not to either one alone - keying progress lookups by enrollment id keeps that lookup O(1) instead of a scan of "every course this user has ever touched."- Quiz scoring is data on the
QuizLesson, not a separateQuizservice. A quiz here is just a lesson with a passing threshold and a way to grade a submitted score; splitting it into a whole grading subsystem would be structure with nothing extra to justify it at this scope. - What's missing for a real system: lesson prerequisites/ordering enforcement, partial
video-resume (seconds watched rather than a binary flag), and retaking a failed quiz with a
cooldown are all real LMS features that slot into
Lesson/Progresswithout changing the shape above - none of them need a new coordinating class.