Shop It Docs
Developer Resourcescourse

Course Module Backend Documentation

Internal architecture, schema contracts, cache/rate-limit strategy, and BullMQ certificate processing runtime.

Course - Backend Documentation

1. Backend Scope and Boundaries

Course backend is a feature-structured NestJS domain that serves three API surfaces:

  • Admin: content management and assessment/certificate configuration
  • Customer/Mobile: discovery, learning runtime, quiz runtime, certificate runtime
  • Public: no-auth certificate verification

Primary boundaries:

  • Commerce dependency: runtime access is entitlement-based via product_access
  • No direct external provider calls in this module path for certificate generation; generation currently persists DB certificate record and can attach pdfUrl later
  • Async work only where needed: certificate generation is queued to BullMQ for durability/idempotency

2. Module Composition (Aggregate + Leaf)

2.1 Module Layout

  • CourseModule
    • providers: CourseService, CourseSharedHelperService
    • controller: PublicCertificateController
    • imports: RedisModule, CourseProcessingModule
  • CourseAdminAggregateModule
    • imports/exports 7 leaf modules:
      • CourseAdminModule
      • TopicAdminModule
      • LessonAdminModule
      • ResourceAdminModule
      • ReviewAdminModule
      • QuizAdminModule
      • CertificateAdminModule
    • note: Swagger includes the leaf admin modules directly, so each feature controller is discoverable without depending on aggregate wrappers
  • CourseCustomerAggregateModule
    • imports/exports 6 leaf modules:
      • DiscoveryCustomerModule
      • PurchaseCustomerModule
      • LearningCustomerModule
      • QuizCustomerModule
      • CertificateCustomerModule
      • CourseReviewCustomerModule
    • note: this aggregate remains available, but mobile composition now uses customer leaf modules directly
  • CourseProcessingModule
    • registers BullMQ queue: QueueName.COURSES
    • provider: CourseProcessor

2.2 Mobile Composition

mobile.module.ts imports customer leaf modules (DiscoveryCustomerModule, PurchaseCustomerModule, LearningCustomerModule, QuizCustomerModule, CertificateCustomerModule, CourseReviewCustomerModule) and mounts them under path: "mobile" via RouterModule.register. Customer controllers remain on base paths (@Controller("courses")), so final route sets are:

  • /api/courses/*
  • /api/mobile/courses/*

3. Data Model (Drizzle / PostgreSQL)

Course schema files live in packages/db/src/schema/course/*.

3.1 Core Tables

TablePurposeNotable constraints
coursetop-level course entityunique slug, unique nullable product_id, FK to category/template, course_access_validity_days_positive_check (accessValidityDays must be > 0 when present; null = lifetime access)
course_topicordered course sectionsunique (course_id, order_index)
course_lessonordered topic lessonsunique (topic_id, order_index)
course_resourceresource attachmentsnum_nonnulls(topic_id, lesson_id) <= 1
course_reviewfake/admin-seeded review recordsrating check 1..5, FK course, required reviewer_name/reviewer_email, optional image_url/image_ref
course_progresslesson completion per userunique (user_id, lesson_id)
course_slug_historyhistorical slugsunique old_slug
quizper-course quiz configunique course_id, checks on attempts/passmark/time
quiz_questionquestion bank per quizunique (quiz_id, order_index)
quiz_attemptuser attempt instanceunique (quiz_id,user_id,attempt_number)
quiz_question_responseper-question scoring rowunique (attempt_id,question_id)
certificate_templatecertificate rendering templateunique name (certificate_template_name_unique), is_active indexed
user_certificateissued certificate recordsunique certificate_number, unique (user_id,course_id)

3.2 Enums Used

  • course_status: draft, published, hidden, archived
  • course_lesson_type: video, live, text, file, link
  • course_review_mode: real, mocked, disabled
  • resource_type: file, link
  • quiz_question_type: multiple_choice, true_false, short_answer

3.3 Referential Integrity Highlights

  • All integer domain FKs are explicit .references(...) with explicit delete behavior.
  • user-bound rows (course_progress, quiz_attempt, user_certificate) use UUID FK to customers.id.
  • ordering contracts are protected by composite unique indexes.
  • quiz/course/resource constraints protect invalid states before service logic runs.

4. Runtime Rules and Domain Invariants

4.1 Course Publication and Product Linkage

CourseSharedHelperService enforces:

  • strict status transitions:
    • draft -> published -> hidden -> archived
  • deterministic slug generation with collision checks against both active and historical slugs
  • product linkage validation:
    • product must exist
    • product must be productType="course"
    • product must not be linked to another course

4.2 Lesson-Type Payload Invariants

Service-level validation ensures required payload by lesson type:

  • video requires videoAssetRef
  • live requires liveSessionRef
  • file requires fileRef
  • link requires linkUrl

4.3 Entitlement Gate

Learning/quiz/certificate customer services require:

  • course linked product exists
  • product_access row for (userId, productId) exists
  • access not expired (accessEndAt check)

Access is granted at order payment time via OrderProcessor.createPostPaymentArtifacts() which calls upsertCourseAccess(). The validity window is determined by courses.accessValidityDays (null = lifetime). Repurchase extends an active window or resets from now.


5. Caching Strategy

5.1 Keyspaces (deterministic via CacheKeyUtil.build)

Admin keyspaces:

  • courses:admin:list:
  • courses:admin:id:
  • courses:admin:topics:
  • courses:admin:lessons:
  • courses:admin:resources:
  • courses:admin:reviews:
  • courses:admin:quiz:
  • courses:admin:quiz:questions:
  • courses:admin:certificate:template:list:
  • courses:admin:certificate:template:id:

Customer keyspaces:

  • courses:customer:list:
  • courses:customer:slug:
  • courses:customer:quiz:detail:
  • courses:customer:reviews:list:
  • courses:customer:reviews:summary:

Cross-module invalidation impact (course writes affecting catalog views):

  • catalog:products:list:
  • catalog:product:slug:
  • catalog:products:featured:
  • legacy prefix pattern for featured list also invalidated

5.2 Invalidation

  • Mutating admin paths call shared invalidation through CourseSharedHelperService.
  • Quiz write services invalidate quiz detail/question keyspaces.
  • Invalidation uses prefix patterns via invalidatePattern(prefix*).

5.3 Cache TTL configuration

  • admin certificate read TTL: COURSE_ADMIN_CERTIFICATE_CACHE_TTL_SECONDS (default 300, fallback REDIS_CACHE_TTL_SECONDS)
  • admin quiz read TTL: COURSE_ADMIN_QUIZ_CACHE_TTL_SECONDS (default 300, fallback REDIS_CACHE_TTL_SECONDS)
  • customer quiz detail TTL: COURSE_CUSTOMER_QUIZ_CACHE_TTL_SECONDS (default 300, fallback REDIS_CACHE_TTL_SECONDS)

6. Rate Limiting Strategy

Controllers implement sliding-window rate limiting in Redis using zset pipeline:

  1. remove expired scores
  2. add request marker (uuid7 request id)
  3. count current window
  4. set key expiry
  5. reject when count exceeds configured threshold

Configured environment variables:

  • common admin/customer fallback:
    • CONTENT_ADMIN_RATE_LIMIT
    • CONTENT_ADMIN_RATE_WINDOW_SECONDS
    • CONTENT_CUSTOMER_ITEM_RATE_LIMIT
    • CONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS
  • public certificate verification specific override:
    • COURSE_CERTIFICATE_VERIFY_RATE_LIMIT
    • COURSE_CERTIFICATE_VERIFY_RATE_WINDOW_SECONDS
    • fallback to the common keys above

Behavior when Redis rate-limit check fails (network/service issues):

  • request is allowed (graceful degradation)
  • warning logged via Nest Logger

7. BullMQ Certificate Processing

7.1 Queue Contract

  • Queue: QueueName.COURSES ("courses")
  • Job: CourseJob.GENERATE_CERTIFICATE ("course.generate_certificate")
  • Payload (GenerateCertificatePayload):
    • userId: string
    • courseId: number
    • requestId?: string
    • correlationId?: string

Producer: CertificateCustomerService.generateCertificate()

  • builds jobId = cert:<userId>:<courseId> for dedupe
  • queue options:
    • attempts = BULL_DEFAULT_ATTEMPTS
    • exponential backoff delay from BULL_DEFAULT_BACKOFF_DELAY_MS
    • defaults in env.validation: attempts 4, backoff delay 2000ms

7.2 Processor Behavior

CourseProcessor flow:

  • logs [start] with correlation and identifiers
  • validates certificate enabled, enrollment, access validity, course completion, quiz pass (if enabled)
  • checks existing certificate first
  • inserts new certificate with:
    • generated certificateNumber format: CERT-YYYYMMDD-XXXXXX
    • onConflictDoNothing(target: [userId, courseId])
    • retry loop for rare certificate_number unique conflict
  • logs [skip], [success], or [failure] outcomes with context

Skip reasons include:

  • certificate_not_enabled
  • not_enrolled
  • access_expired
  • course_incomplete
  • quiz_not_passed
  • already_exists

7.3 Correlation and IDs

  • request/correlation IDs generated with UUIDv7 in producer and rate-limit flows.
  • processor resolves correlation from payload, job id fallback, or fresh UUIDv7.

8. Error and Resilience Contracts

8.1 Domain exceptions

HTTPerrorCodeMessage
400QUIZ_MAX_ATTEMPTS_REACHEDquizAttempts must be greater than 0.
400QUIZ_INVALID_ANSWER_FORMATpassMark must be between 0 and 100.
400QUIZ_INVALID_ANSWER_FORMATtimeLimit must be greater than 0.
400QUIZ_ANSWER_INVALID_TYPEmultiple_choice questions require at least 2 options.
400QUIZ_ANSWER_INVALID_TYPEmultiple_choice correctAnswer must be a string.
400QUIZ_ANSWER_INVALID_TYPEcorrectAnswer cannot be empty.
400QUIZ_ANSWER_INVALID_TYPEcorrectAnswer must match one of the provided options.
400QUIZ_ANSWER_INVALID_TYPEtrue_false questions require options ["true", "false"].
400QUIZ_ANSWER_INVALID_TYPEtrue_false correctAnswer must be a string.
400QUIZ_ANSWER_INVALID_TYPEtrue_false correctAnswer must be either "true" or "false".
400QUIZ_ANSWER_INVALID_TYPEshort_answer correctAnswer array must contain at least one value.
400QUIZ_ANSWER_INVALID_TYPEshort_answer correctAnswer must be a string or string array.
400QUIZ_ANSWER_INVALID_TYPEoptions must be provided as an array.
400QUIZ_ANSWER_INVALID_TYPEoptions cannot contain empty values.
400COURSE_TOPIC_CREATE_FAILEDFailed to create topic.
400COURSE_LESSON_CREATE_FAILEDFailed to create lesson.
400COURSE_RESOURCE_CREATE_FAILEDFailed to create resource.
400COURSE_REVIEW_CREATE_FAILEDFailed to create review.
400COURSE_REVIEW_INVALID_RATINGRating must be between 1 and 5.
400COURSE_REVIEW_UPLOAD_FAILEDReview image upload payload or operation failed.
400COURSE_LINK_FAILEDFailed to create course.
400COURSE_SELLABLE_REQUIREDOnly products with productType 'course' can be linked.
400COURSE_LESSON_MISMATCHTopic/Lesson/Resource/Review does not belong to the course.
400QUIZ_EXISTS_FOR_COURSEQuiz already exists for this course.
400QUIZ_UNABLE_TO_STARTUnable to start quiz attempt.
400QUIZ_COURSE_INCOMPLETECourse completion is required before starting quiz.
400QUIZ_ALREADY_SUBMITTEDThis attempt has already been submitted.
400QUIZ_TIME_LIMIT_EXCEEDEDTime limit exceeded.
400QUIZ_INVALID_ANSWER_FORMATInvalid answer format for question.
400QUIZ_ORDER_INDEX_REQUIREDorderIndex is required.
400QUIZ_REORDER_INCOMPLETEquestionIds must not be empty / must contain all questions.
400QUIZ_REORDER_DUPLICATE_IDSquestionIds must be unique.
400QUIZ_REORDER_INVALID_IDSOne or more question/topic IDs do not belong.
400QUIZ_REORDER_INVALID_SEQUENCEInvalid question ID sequence.
400QUIZ_QUESTION_MISMATCHQuestion does not belong to the course quiz.
400CERTIFICATE_CREATE_FAILEDFailed to create certificate template.
400CERTIFICATE_TEMPLATE_HAS_PUBLISHED_COURSESCannot deactivate/delete template while linked published courses reference it.
400CERTIFICATE_QUEUE_FAILEDFailed to queue certificate generation.
400COURSE_ACCESS_EXPIREDCourse access has expired.
403COURSE_ACCESS_DENIEDYou do not have access to this course.
403COURSE_ACCESS_DENIEDYou are not allowed to access this course certificate.
404COURSE_NOT_FOUNDCourse not found.
404COURSE_TOPIC_NOT_FOUNDTopic not found.
404COURSE_LESSON_NOT_FOUNDLesson not found.
404COURSE_RESOURCE_NOT_FOUNDResource not found.
404COURSE_REVIEW_NOT_FOUNDReview not found.
404COURSE_PRODUCT_NOT_FOUNDProduct not found.
404COURSE_CATEGORY_NOT_FOUNDCategory not found.
404COURSE_CUSTOMER_NOT_FOUNDCustomer not found.
404QUIZ_NOT_FOUNDQuiz not found.
404QUIZ_ATTEMPT_NOT_FOUNDQuiz attempt not found.
404QUIZ_QUESTION_NOT_FOUNDQuestion not found.
404COURSE_PURCHASE_NOT_FOUNDPurchase not found.
404CERTIFICATE_NOT_FOUNDCertificate not found.
404CERTIFICATE_TEMPLATE_NOT_FOUNDCertificate template not found.
409COURSE_LINK_FAILEDProduct is already linked to another course.
429RATE_LIMIT_EXCEEDEDToo many requests. Please try again later.
500CERTIFICATE_NUMBER_GENERATION_FAILEDUnable to generate a unique certificate number.

8.2 Idempotency points

  • lesson completion path is effectively idempotent through user+lesson progress uniqueness
  • certificate generation:
    • queue dedupe via deterministic jobId
    • DB dedupe via unique (user_id, course_id) + onConflictDoNothing

8.3 Atomicity patterns

  • multi-row ordering and mutation operations are wrapped in service-layer transactional patterns where needed (topic/lesson/question reorder/write flows)
  • certificate insert path retries conflict and converges to existing row when already generated

9. Performance Notes

  • list APIs use pagination normalization + optional count query behavior
  • search/sort queries use explicit projections (no SELECT *)
  • independent reads use Promise.all in key runtime paths (eligibility and quiz detail)
  • Redis caches target high-frequency list/detail/read paths
  • processor avoids duplicate heavy work by fast skip checks before insert path

10. Backend Diagrams

10.1 Learning + Quiz + Certificate Runtime

10.2 Admin Composition

11. File Map

ConcernFile PathPurpose
Admin courseapps/api/src/modules/course/admin/course/*Course CRUD
Admin topicapps/api/src/modules/course/admin/topic/*Topic management
Admin lessonapps/api/src/modules/course/admin/lesson/*Lesson management
Admin resourceapps/api/src/modules/course/admin/resource/*Resource management
Admin reviewapps/api/src/modules/course/admin/review/*Review management
Admin quizapps/api/src/modules/course/admin/quiz/*Quiz configuration
Admin certificateapps/api/src/modules/course/admin/certificate/*Certificate templates
Customer discoveryapps/api/src/modules/course/customer/discovery/*Course discovery
Customer learningapps/api/src/modules/course/customer/learning/*Lesson access
Customer quizapps/api/src/modules/course/customer/quiz/*Quiz runtime
Customer certificateapps/api/src/modules/course/customer/certificate/*Certificate generation
Processingapps/api/src/modules/course/processing/*BullMQ processor
DB schemapackages/db/src/schema/course/*Table definitions

12. Environment Variables

VariableDefaultDescription
CONTENT_ADMIN_RATE_LIMIT100Rate limit for admin content operations
CONTENT_ADMIN_RATE_WINDOW_SECONDS60Rate limit window for admin operations
CONTENT_CUSTOMER_ITEM_RATE_LIMIT30Rate limit for customer content item requests
CONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS60Rate limit window for customer item requests
COURSE_CERTIFICATE_VERIFY_RATE_LIMIT-Rate limit for public certificate verification
COURSE_CERTIFICATE_VERIFY_RATE_WINDOW_SECONDS-Rate limit window for certificate verification
COURSE_ADMIN_CERTIFICATE_CACHE_TTL_SECONDS300Cache TTL for admin certificate views
COURSE_ADMIN_QUIZ_CACHE_TTL_SECONDS300Cache TTL for admin quiz views
COURSE_CUSTOMER_QUIZ_CACHE_TTL_SECONDS300Cache TTL for customer quiz detail
BULL_DEFAULT_ATTEMPTS4Default BullMQ job retry attempts
BULL_DEFAULT_BACKOFF_DELAY_MS2000Default backoff delay in milliseconds

Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.


See Also