Code an online course platform
Open in the interactive app: practice, quiz, mock interview →In one minute
- A learning platform looks like a catalogue of videos, but its hard parts are a learner's progress and the course changing underneath it. Model a Course as a series of immutable CourseVersions, and pin each Enrollment to the version the learner started.
- Progress is a projection of completion events: the set of lessons done in the pinned version. Completion events arrive many times (players retry, phones reconnect), so recording one must be idempotent.
- A quiz lesson is complete when the best score reaches the pass mark. Keep the best score, not the last one, so a later bad attempt never takes credit away.
- Prerequisites form a directed graph between courses and must stay acyclic. Check for a cycle on every publish, because a new version can add an edge that closes a loop.
- The problem shape is duplicates (the same "completed" event many times) plus multi-step consistency (the last lesson, the 100% progress and the certificate must agree). Release pinning has the same shape: a running build is pinned to the dependency versions it resolved, and upgrading is a deliberate migration.
Requirements
Functional
- Instructors publish courses made of ordered lessons. Each lesson is a
videoor aquiz. A course has a quiz pass mark and may list prerequisite courses. - Publishing again creates a new version. Learners already enrolled keep their version until they choose to upgrade; new learners get the latest.
- Learners enroll only after earning a certificate in every prerequisite of the latest version.
- Learners complete video lessons and submit quiz scores. Progress is the percentage of the pinned version's lessons that are complete.
- On reaching 100%, the learner gets a certificate once, stamped with the time and the version they completed.
Non-functional
- Recording a completion twice has the same effect as recording it once.
- Progress is exact and explainable from the stored completions.
- Content changes never silently change a learner's progress or revoke a certificate.
Out of scope
- Video delivery and transcoding (see Design YouTube), payments, reviews, discussion forums, and grading code submissions (see Design LeetCode).
Entities and relationships
Course is an identity: an id, an owner and a list of versions. It has no lessons of its own. CourseVersion is an immutable snapshot: a version number, an ordered list of Lesson objects, the prerequisite course ids, and the quiz pass mark. Lessons are identified by stable ids that survive across versions, so "lesson intro" in version 1 and version 2 is the same lesson for progress purposes.
Lesson has an id, a title and a kind. VideoLesson knows its duration; QuizLesson knows its questions. They differ in what "complete" means: a video is complete when watched, a quiz when the best score reaches the pass mark. That difference is behaviour, so it lives in the lesson type, not in if kind == "quiz" checks across the codebase.
Enrollment joins a learner to a course and pins a version. It holds the completed lesson ids, each quiz's best score, and an optional Certificate. Certificate is a value object: an id, the learner, the course, the version completed and the issue time. Once issued it never changes.
LearningPlatform is the facade: publish, enroll, record a completion, upgrade, and query progress and certificates. It takes an injected Clock for certificate timestamps.
Class diagram
classDiagram
class Course {
+id
+versions list~CourseVersion~
+latest() CourseVersion
}
class CourseVersion {
+number int
+lessons list~Lesson~
+prereqs list
+passMark int
}
class Lesson {
<<abstract>>
+id
+isComplete(enrollment, passMark) bool
}
class VideoLesson {
+durationSec int
}
class QuizLesson {
+questions list
}
class Enrollment {
+learner
+version int
+done set
+bestScores map
+progress() int
}
class Certificate {
+id
+version int
+issuedAt int
}
class LearningPlatform {
+publish(courseId, lessons, prereqs, passMark) int
+enroll(learner, courseId) int
+complete(learner, courseId, lessonId, score) bool
+upgrade(learner, courseId) int
+certificate(learner, courseId) Certificate
}
Course "1" *-- "*" CourseVersion
CourseVersion "1" *-- "*" Lesson
Lesson <|-- VideoLesson
Lesson <|-- QuizLesson
Enrollment --> CourseVersion
Enrollment "1" o-- "0..1" Certificate
LearningPlatform --> Course
LearningPlatform --> EnrollmentKey flows
The flow that ties the design together is the last lesson. Mia has finished two of three lessons and submits the final quiz. The facade records the best score, checks the pass mark, recomputes progress, and issues the certificate in the same step, so no reader can see 100% without a certificate.
sequenceDiagram
participant M as Mia
participant P as LearningPlatform
participant E as Enrollment
participant V as CourseVersion
participant C as Clock
M->>P: complete(mia, sql101, final-quiz, 85)
P->>E: find(mia, sql101)
E-->>P: pinned version 1
P->>V: lesson(final-quiz)
V-->>P: quiz, pass mark 70
P->>E: best = max(best, 85), mark done
P->>E: progress()
E-->>P: 100
P->>C: now()
C-->>P: 1700000000
P->>E: attach Certificate(CERT-7, v1, 1700000000)
P-->>M: trueIf the same request is retried after a timeout, the best score stays 85, the lesson is already done, and a certificate already exists, so nothing changes. Idempotency comes from the shape of the state (a set and a max), not from a deduplication table.
Design patterns used and why
Immutable snapshot (versioning). Each publish produces a new CourseVersion and never edits an old one. Enrollments point at a version, so an instructor deleting a lesson cannot move a learner from 100% to 90% or invalidate the version their certificate names. This is the same move as pinning a dependency lock file.
Polymorphism for lesson kinds. Lesson.isComplete(enrollment, passMark) is overridden by VideoLesson and QuizLesson. Adding a CodingExercise that is complete when hidden tests pass is a new subclass; progress code does not change.
Composite for structure. Real courses nest: sections contain lessons, and a "specialisation" contains courses. A Composite lets progress be computed the same way at every level (fraction of leaves complete), and lets a weighted rule replace the plain count later.
Observer for side effects. LessonCompleted and CertificateIssued events drive emails, streaks, analytics and recommendations. These subscribers should never be able to make recording a completion fail.
Facade with an injected clock. One method per learner action, and time read through clock(), so tests can assert exact certificate timestamps.
Concurrency and edge cases
Duplicate and out-of-order completion events
A video player posts "completed" at the end, again on replay, and again when a phone that was offline reconnects. Store completions as a set keyed by (learner, course, lesson), or in a database as a unique key with INSERT ... ON CONFLICT DO NOTHING. Quiz scores fold with max. Both operations are commutative and idempotent, so arrival order and repeats do not matter. The xAPI standard bakes the same idea into its protocol: each statement carries a client-chosen UUID, and a learning record store that receives an id it already has should check the content matches and answer 409 Conflict if it does not.
Two final lessons finishing at once
On two devices, Mia completes the last video and the last quiz within a millisecond. Each request sees progress at 100% and tries to issue a certificate. Under a per-enrollment lock (or a row lock on the enrollment), the second request sees the certificate already attached and does nothing. Without it, she gets two certificates with different ids, which is exactly what a verification page cannot tolerate.
Prerequisite cycles
Course A requires B. Later, B version 2 adds A as a prerequisite. Nobody can now enroll in either. On publish, run a depth-first search from each new prerequisite along the latest versions' edges. If it reaches the course being published, reject the publish. The graph has one node per course, so this is cheap.
Upgrading a learner
Upgrading maps completions by stable lesson id. A lesson that disappeared is dropped, and a new lesson starts incomplete. A quiz carried over is re-checked against the new pass mark, because a version can raise it. The certificate, if any, is untouched: it names the version it was earned on.
Which version's prerequisites?
Enrollment checks the latest version's prerequisites, because that is what the learner is enrolling in. Changing prerequisites never removes existing enrollments.
Down the stack
Layer 1The contract
For each enrollment, progress = floor(100 * |done ∩ lessons(v)| / |lessons(v)|) where v is the pinned version. Recording the same completion again changes nothing. A quiz is done exactly when its best score is at least the version's pass mark. A certificate is issued at most once per enrollment, at the first moment progress reaches 100, and is never modified. The prerequisite graph over latest versions is acyclic. A rejected call changes nothing.
Layer 2The mechanism
A course stores versions in a list indexed by number, so pinning is just an integer. An enrollment keeps a set of done lesson ids and a map of best scores. The complete method is a set insert and a max, and progress is a count over the pinned version's lesson list. Publishing validates the lessons (non-empty, unique ids, known kinds), then runs a DFS over prerequisite edges, which costs O(courses + edges) in the worst case. At scale, completions are an append-only event table (learner, course, lesson, score, ts, event_id), and the enrollment row holds the denormalised done set and percentage, rebuilt from events if ever in doubt. That is event sourcing in its most ordinary form.
Layer 3The machine
A 200-lesson course fits its done set into a 200-bit bitmap: 25 bytes, four 64-bit words. With lesson positions assigned per version, progress is four POPCNT instructions, which modern x86 cores retire at about one per cycle, compared with hashing 200 strings. A million active enrollments then cost 25 MB of bitmaps, which fits in one cache server's memory. The catch is that positions are per version, so upgrade must remap bits through the lesson ids, which is one more reason upgrades are explicit. The event table grows by roughly 100 bytes per row. Ten million completions a day is about 1 GB a day, cheap to keep forever and enough to rebuild any learner's state.
Why it's built this way
Instructors fix and reorder content constantly, but learners and employers need progress and certificates that do not shift, so content is versioned and enrollments pin a version. Networks and players retry, so completion is recorded as set membership and best score, both naturally idempotent. A certificate is a promise to third parties, so it is immutable and issued exactly once under a lock. Prerequisite edges come from many instructors who cannot see each other's courses, so the platform, not the authors, guarantees there is no cycle.
Extensibility
- Weighted progress. Videos counted by minutes or graded quizzes worth more: progress becomes a Strategy on the version, and the Composite structure computes it per section.
- Deadlines and cohorts. A cohort enrollment adds due dates per lesson, evaluated with the injected clock. Late completions still count but are flagged.
- Attempt limits.
QuizLessongainsmaxAttempts; the enrollment counts attempts per quiz and rejects extra ones before recording a score. - Certificate verification. Publish a public page per certificate id, and sign the (id, learner, course, version, time) tuple so employers can check it offline.
Leadership lens
- Decide the versioning policy with product before launch: do learners auto-upgrade, get prompted, or stay pinned? Changing it later means migrating every enrollment and explaining moved progress bars to support.
- Treat the certificate as a legal and reputational artifact. Require exactly-once issuance tests under concurrency and an audit trail from certificate back to the completion events.
- Instrument duplicate completion rates per client version. A spike usually means a player bug, and idempotent storage hides it from users, but not from your network bill.
- Build the event log first and the projections second. A wrong percentage can be recomputed, but lost events cannot be.
Check yourself
An instructor removes a lesson from a course that Mia is halfway through. What happens to her progress?
Show answer
Nothing. The edit is a new version and Mia stays pinned to hers. If she upgrades, her completions are mapped by lesson id, the removed lesson drops out, and her percentage is recomputed over the new version.
Why keep the best quiz score rather than the latest?
Show answer
So that completion is monotonic and idempotent: a later poor attempt or a replayed older event can never undo a pass, and repeated submissions in any order produce the same state.
Two devices finish Mia's last two lessons simultaneously. What guarantees one certificate?
Show answer
Issuance happens in the same critical section as the completion, under a per-enrollment lock or row lock. The second request sees the attached certificate and does nothing.
Course A already requires B. Why is it not enough to check only that B exists when A is published?
Show answer
Existence does not prevent B from later adding A as its own prerequisite. Each publish must check that none of its new prerequisites can reach the course itself.
Watch
- System Design 19: Design of Online Learning Platform (InterviewWithBunny): walks through a Udemy or Coursera style platform at both the high and low level.
Free resources
- xAPI specification (ADL): the standard for recording learning events as statements, including statement ids and how a learning record store treats duplicates.
- SCORM explained (Rustici Software): the older standard for packaging courses and tracking completion, still used by most corporate learning systems.
- Event Sourcing (Martin Fowler): the idea behind progress as a projection of completion events.
- Topological sorting (Wikipedia): prerequisite graphs, ordering and cycle detection.
- Composite pattern (Refactoring.Guru): the structure for sections, lessons and multi-course programmes.
- Hamming weight (Wikipedia): population count, the instruction behind counting completed lessons in a bitmap.