Low-Level Design: Online Learning Platform (Coursera/Udemy) — Courses, Progress, and Certificates

Core Entities

Course: course_id, title, description, instructor_id, category, level (BEGINNER, INTERMEDIATE, ADVANCED), language, price, published_at, status (DRAFT, PUBLISHED, ARCHIVED), total_enrolled. Lesson: lesson_id, course_id, title, content_type (VIDEO, ARTICLE, QUIZ), content_url, duration_seconds, order_index, is_free_preview. Enrollment: enrollment_id, student_id, course_id, enrolled_at, completed_at (nullable), completion_percentage, last_accessed_at, status (ACTIVE, COMPLETED, REFUNDED). LessonProgress: progress_id, enrollment_id, lesson_id, status (NOT_STARTED, IN_PROGRESS, COMPLETED), completed_at, watch_position_seconds (for videos). Certificate: cert_id, enrollment_id, student_id, course_id, issued_at, verification_code (UUID), credential_url.

Course Enrollment and Access Control

class EnrollmentService:
    def enroll(self, student_id: int, course_id: int, payment_intent_id: str) -> Enrollment:
        # Idempotency: check existing enrollment
        existing = self.db.get_enrollment(student_id, course_id)
        if existing and existing.status == EnrollmentStatus.ACTIVE:
            return existing

        course = self.db.get_course(course_id)
        if course.status != CourseStatus.PUBLISHED:
            raise CourseNotAvailableError()

        # Verify payment
        if course.price > 0:
            payment = self.payment.verify(payment_intent_id, course.price)
            if not payment.succeeded:
                raise PaymentError()

        enrollment = Enrollment(
            student_id=student_id,
            course_id=course_id,
            enrolled_at=datetime.now(),
            status=EnrollmentStatus.ACTIVE
        )
        self.db.save(enrollment)
        self.db.increment_course_enrollment_count(course_id)
        self.email.send_welcome(student_id, course_id)
        return enrollment

    def can_access_lesson(self, student_id: int, lesson_id: int) -> bool:
        lesson = self.db.get_lesson(lesson_id)
        if lesson.is_free_preview:
            return True
        enrollment = self.db.get_enrollment(student_id, lesson.course_id)
        return enrollment is not None and enrollment.status == EnrollmentStatus.ACTIVE

Progress Tracking

Track lesson-level progress: when a student watches a video, the client sends periodic progress pings every 30 seconds: {enrollment_id, lesson_id, watch_position_seconds}. Update LessonProgress.watch_position_seconds (allows resuming from where they left off). Mark lesson as COMPLETED when: video watched > 90% of duration, article scrolled to the bottom, quiz score >= passing threshold. Course completion calculation: completion_percentage = completed_lessons / total_lessons * 100. Recalculate and update Enrollment.completion_percentage on each lesson completion. Mark Enrollment.status = COMPLETED and issue a certificate when completion_percentage = 100.

Certificate Issuance and Verification

On course completion: generate a Certificate record with a unique verification_code (UUID v4). Generate a PDF certificate using a template (student name, course title, completion date, instructor name). Upload to S3, store the URL in Certificate.credential_url. Send the certificate URL to the student via email. Verification page: GET /verify/{verification_code} — look up the certificate by verification_code, display the student name, course, and completion date. This allows employers to verify authenticity. LinkedIn integration: provide a “Add to LinkedIn” URL that pre-populates the certification in the student’s LinkedIn profile (LinkedIn supports a URL scheme for this). Signed certificates: embed a digital signature in the PDF using the platform’s private key, verifiable with the public key — prevents forgery.

Course Search and Recommendations

Search: Elasticsearch index with course title, description, instructor name, and tags. Full-text search with relevance scoring. Filters: category, level, language, price (free vs paid), rating. Sort by: relevance, enrollment count, rating, date. Recommendations: collaborative filtering (students who enrolled in course A also enrolled in course B) — store co-enrollment matrix, updated nightly. Content-based filtering: if a student completed 3 Python courses, recommend more Python courses. Implement with a user-item matrix: rows are students, columns are courses, values are engagement score (completed=1, in_progress=0.5). Matrix factorization (SVD) gives latent factors for recommendation. For cold start (new student, no history): recommend top-rated courses in their declared interest areas (onboarding survey).

Video Delivery

Store course videos in S3. Transcode to multiple resolutions on upload (720p, 1080p) — trigger a Lambda on S3 upload. Generate HLS manifests for adaptive streaming. Serve through a CDN (CloudFront) — students worldwide get low-latency playback. Access control: pre-signed CDN URLs or signed cookies scoped to the student’s enrollment. The URL expires after 24 hours; the client refreshes via an API call. Never serve video through the application server — always directly from the CDN. For large video files: multipart upload to S3 so instructors can upload 2GB+ files without timeout. Progress tracking pings go to the application server, not the CDN.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you track video watch progress and enable resuming?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The video player sends periodic progress pings to the server every 30 seconds: {enrollment_id, lesson_id, watch_position_seconds}. Server updates LessonProgress.watch_position_seconds. On play: load the stored position and seek the video to that point. The ping interval (30 seconds) is a trade-off: shorter intervals give more precise resume points but add server load; longer intervals risk losing more progress if the session ends suddenly. For a 1-hour course with 100,000 concurrent learners: 100,000 pings/30s = 3,333 writes/second — manageable with async writes to a database write-behind cache (Redis as buffer, flushed to the DB every 60 seconds). Mark lesson COMPLETED when watch_position_seconds > 0.9 * duration_seconds. Never require 100% — the last few seconds are credits/silence and frustrate learners.”
}
},
{
“@type”: “Question”,
“name”: “How do you prevent certificate fraud and enable employer verification?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each certificate has a unique verification_code (UUID v4 or a shorter alphanumeric code). The verification page at /verify/{code} looks up the certificate and displays: student name, course title, instructor, completion date, and the platform’s branding. This page is publicly accessible — no login required — so employers can verify directly. Tamper prevention: the certificate PDF is generated with a digital signature (platform’s private key, RSA or Ed25519). The signature covers the student name, course, and date. Employers can verify the signature with the platform’s public key. Store the verification URL on the certificate (embedded QR code linking to /verify/{code}). LinkedIn integration: the “Add to LinkedIn” button links to linkedin.com/profile/add/… with prefilled fields (organization name, certification name, issue date, credential URL). LinkedIn stores the credential URL, so employers can click through to verify.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement course recommendations for new users (cold start)?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “New users have no course history — collaborative filtering cannot be applied. Cold start strategies: (1) Onboarding survey: ask the new user about their goals (career change, skill development), experience level, and topics of interest. Map answers to course categories and recommend top-rated courses in those categories. (2) Trending courses: globally popular courses in the past 7 days (weighted by enrollment velocity, not just total enrollments — captures what is hot now). (3) Curated bundles: editorial “learning paths” (Python for Beginners, Full-Stack Web Developer) presented to new users by declared goal. (4) Social proof: “Join 50,000 learners who started with this course.” After a user completes 2-3 courses, there is enough behavioral data to switch to collaborative filtering. Track cold-start recommendation click-through rate separately to measure the quality of each strategy.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle course refunds and access revocation?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Refund policy: most platforms offer a 30-day money-back guarantee. On refund request: verify the request is within 30 days of enrollment and the student has not completed > 30% of the course (to prevent “watch, complete, refund” abuse). If eligible: call the payment gateway to issue a refund. Update Enrollment.status = REFUNDED. Access revocation: the can_access_lesson check queries enrollment status — REFUNDED enrollments are treated the same as no enrollment. Implement access revocation asynchronously: publish an enrollment.refunded event; the content service subscribes and revokes CDN-signed URL cookies for that student-course pair. Edge case: a student who downloaded offline content (mobile app). Flag the enrollment as revoked in the app’s local database on next sync; the app stops serving offline content.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement a discussion forum for course Q&A?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store threads per lesson: Thread (thread_id, lesson_id, author_id, title, body, vote_count, reply_count, created_at, is_pinned). Reply (reply_id, thread_id, author_id, body, vote_count, is_instructor, created_at). Fetching threads for a lesson: SELECT threads WHERE lesson_id = :id ORDER BY is_pinned DESC, vote_count DESC, created_at DESC. Pagination with keyset (last_seen_vote_count, last_seen_created_at). Upvoting: Redis INCR thread:{id}:votes (fast), flush to DB periodically. Instructor answer highlighting: is_instructor flag on replies — sort instructor replies first. Notifications: when a thread gets a reply, notify the thread author. When the instructor replies, notify all thread subscribers (students who replied or upvoted). Full-text search on threads: Elasticsearch index updated via CDC. For a large course (100K students), a lesson thread can accumulate 10K posts — only fetch the top 20 by default with “load more.””
}
}
]
}

Asked at: LinkedIn Interview Guide

Asked at: Atlassian Interview Guide

Asked at: Shopify Interview Guide

Asked at: Snap Interview Guide

Scroll to Top