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
pdfUrllater - 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
- providers:
CourseAdminAggregateModule- imports/exports 7 leaf modules:
CourseAdminModuleTopicAdminModuleLessonAdminModuleResourceAdminModuleReviewAdminModuleQuizAdminModuleCertificateAdminModule
- note: Swagger includes the leaf admin modules directly, so each feature controller is discoverable without depending on aggregate wrappers
- imports/exports 7 leaf modules:
CourseCustomerAggregateModule- imports/exports 6 leaf modules:
DiscoveryCustomerModulePurchaseCustomerModuleLearningCustomerModuleQuizCustomerModuleCertificateCustomerModuleCourseReviewCustomerModule
- note: this aggregate remains available, but mobile composition now uses customer leaf modules directly
- imports/exports 6 leaf modules:
CourseProcessingModule- registers BullMQ queue:
QueueName.COURSES - provider:
CourseProcessor
- registers BullMQ queue:
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
| Table | Purpose | Notable constraints |
|---|---|---|
course | top-level course entity | unique 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_topic | ordered course sections | unique (course_id, order_index) |
course_lesson | ordered topic lessons | unique (topic_id, order_index) |
course_resource | resource attachments | num_nonnulls(topic_id, lesson_id) <= 1 |
course_review | fake/admin-seeded review records | rating check 1..5, FK course, required reviewer_name/reviewer_email, optional image_url/image_ref |
course_progress | lesson completion per user | unique (user_id, lesson_id) |
course_slug_history | historical slugs | unique old_slug |
quiz | per-course quiz config | unique course_id, checks on attempts/passmark/time |
quiz_question | question bank per quiz | unique (quiz_id, order_index) |
quiz_attempt | user attempt instance | unique (quiz_id,user_id,attempt_number) |
quiz_question_response | per-question scoring row | unique (attempt_id,question_id) |
certificate_template | certificate rendering template | unique name (certificate_template_name_unique), is_active indexed |
user_certificate | issued certificate records | unique certificate_number, unique (user_id,course_id) |
3.2 Enums Used
course_status:draft,published,hidden,archivedcourse_lesson_type:video,live,text,file,linkcourse_review_mode:real,mocked,disabledresource_type:file,linkquiz_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 tocustomers.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:
videorequiresvideoAssetRefliverequiresliveSessionReffilerequiresfileReflinkrequireslinkUrl
4.3 Entitlement Gate
Learning/quiz/certificate customer services require:
- course linked product exists
product_accessrow for(userId, productId)exists- access not expired (
accessEndAtcheck)
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(default300, fallbackREDIS_CACHE_TTL_SECONDS) - admin quiz read TTL:
COURSE_ADMIN_QUIZ_CACHE_TTL_SECONDS(default300, fallbackREDIS_CACHE_TTL_SECONDS) - customer quiz detail TTL:
COURSE_CUSTOMER_QUIZ_CACHE_TTL_SECONDS(default300, fallbackREDIS_CACHE_TTL_SECONDS)
6. Rate Limiting Strategy
Controllers implement sliding-window rate limiting in Redis using zset pipeline:
- remove expired scores
- add request marker (
uuid7request id) - count current window
- set key expiry
- reject when count exceeds configured threshold
Configured environment variables:
- common admin/customer fallback:
CONTENT_ADMIN_RATE_LIMITCONTENT_ADMIN_RATE_WINDOW_SECONDSCONTENT_CUSTOMER_ITEM_RATE_LIMITCONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS
- public certificate verification specific override:
COURSE_CERTIFICATE_VERIFY_RATE_LIMITCOURSE_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: stringcourseId: numberrequestId?: stringcorrelationId?: 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 delay2000ms
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
certificateNumberformat:CERT-YYYYMMDD-XXXXXX onConflictDoNothing(target: [userId, courseId])- retry loop for rare
certificate_numberunique conflict
- generated
- logs
[skip],[success], or[failure]outcomes with context
Skip reasons include:
certificate_not_enablednot_enrolledaccess_expiredcourse_incompletequiz_not_passedalready_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
| HTTP | errorCode | Message |
|---|---|---|
| 400 | QUIZ_MAX_ATTEMPTS_REACHED | quizAttempts must be greater than 0. |
| 400 | QUIZ_INVALID_ANSWER_FORMAT | passMark must be between 0 and 100. |
| 400 | QUIZ_INVALID_ANSWER_FORMAT | timeLimit must be greater than 0. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | multiple_choice questions require at least 2 options. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | multiple_choice correctAnswer must be a string. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | correctAnswer cannot be empty. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | correctAnswer must match one of the provided options. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | true_false questions require options ["true", "false"]. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | true_false correctAnswer must be a string. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | true_false correctAnswer must be either "true" or "false". |
| 400 | QUIZ_ANSWER_INVALID_TYPE | short_answer correctAnswer array must contain at least one value. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | short_answer correctAnswer must be a string or string array. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | options must be provided as an array. |
| 400 | QUIZ_ANSWER_INVALID_TYPE | options cannot contain empty values. |
| 400 | COURSE_TOPIC_CREATE_FAILED | Failed to create topic. |
| 400 | COURSE_LESSON_CREATE_FAILED | Failed to create lesson. |
| 400 | COURSE_RESOURCE_CREATE_FAILED | Failed to create resource. |
| 400 | COURSE_REVIEW_CREATE_FAILED | Failed to create review. |
| 400 | COURSE_REVIEW_INVALID_RATING | Rating must be between 1 and 5. |
| 400 | COURSE_REVIEW_UPLOAD_FAILED | Review image upload payload or operation failed. |
| 400 | COURSE_LINK_FAILED | Failed to create course. |
| 400 | COURSE_SELLABLE_REQUIRED | Only products with productType 'course' can be linked. |
| 400 | COURSE_LESSON_MISMATCH | Topic/Lesson/Resource/Review does not belong to the course. |
| 400 | QUIZ_EXISTS_FOR_COURSE | Quiz already exists for this course. |
| 400 | QUIZ_UNABLE_TO_START | Unable to start quiz attempt. |
| 400 | QUIZ_COURSE_INCOMPLETE | Course completion is required before starting quiz. |
| 400 | QUIZ_ALREADY_SUBMITTED | This attempt has already been submitted. |
| 400 | QUIZ_TIME_LIMIT_EXCEEDED | Time limit exceeded. |
| 400 | QUIZ_INVALID_ANSWER_FORMAT | Invalid answer format for question. |
| 400 | QUIZ_ORDER_INDEX_REQUIRED | orderIndex is required. |
| 400 | QUIZ_REORDER_INCOMPLETE | questionIds must not be empty / must contain all questions. |
| 400 | QUIZ_REORDER_DUPLICATE_IDS | questionIds must be unique. |
| 400 | QUIZ_REORDER_INVALID_IDS | One or more question/topic IDs do not belong. |
| 400 | QUIZ_REORDER_INVALID_SEQUENCE | Invalid question ID sequence. |
| 400 | QUIZ_QUESTION_MISMATCH | Question does not belong to the course quiz. |
| 400 | CERTIFICATE_CREATE_FAILED | Failed to create certificate template. |
| 400 | CERTIFICATE_TEMPLATE_HAS_PUBLISHED_COURSES | Cannot deactivate/delete template while linked published courses reference it. |
| 400 | CERTIFICATE_QUEUE_FAILED | Failed to queue certificate generation. |
| 400 | COURSE_ACCESS_EXPIRED | Course access has expired. |
| 403 | COURSE_ACCESS_DENIED | You do not have access to this course. |
| 403 | COURSE_ACCESS_DENIED | You are not allowed to access this course certificate. |
| 404 | COURSE_NOT_FOUND | Course not found. |
| 404 | COURSE_TOPIC_NOT_FOUND | Topic not found. |
| 404 | COURSE_LESSON_NOT_FOUND | Lesson not found. |
| 404 | COURSE_RESOURCE_NOT_FOUND | Resource not found. |
| 404 | COURSE_REVIEW_NOT_FOUND | Review not found. |
| 404 | COURSE_PRODUCT_NOT_FOUND | Product not found. |
| 404 | COURSE_CATEGORY_NOT_FOUND | Category not found. |
| 404 | COURSE_CUSTOMER_NOT_FOUND | Customer not found. |
| 404 | QUIZ_NOT_FOUND | Quiz not found. |
| 404 | QUIZ_ATTEMPT_NOT_FOUND | Quiz attempt not found. |
| 404 | QUIZ_QUESTION_NOT_FOUND | Question not found. |
| 404 | COURSE_PURCHASE_NOT_FOUND | Purchase not found. |
| 404 | CERTIFICATE_NOT_FOUND | Certificate not found. |
| 404 | CERTIFICATE_TEMPLATE_NOT_FOUND | Certificate template not found. |
| 409 | COURSE_LINK_FAILED | Product is already linked to another course. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. Please try again later. |
| 500 | CERTIFICATE_NUMBER_GENERATION_FAILED | Unable 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
- queue dedupe via deterministic
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.allin 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
| Concern | File Path | Purpose |
|---|---|---|
| Admin course | apps/api/src/modules/course/admin/course/* | Course CRUD |
| Admin topic | apps/api/src/modules/course/admin/topic/* | Topic management |
| Admin lesson | apps/api/src/modules/course/admin/lesson/* | Lesson management |
| Admin resource | apps/api/src/modules/course/admin/resource/* | Resource management |
| Admin review | apps/api/src/modules/course/admin/review/* | Review management |
| Admin quiz | apps/api/src/modules/course/admin/quiz/* | Quiz configuration |
| Admin certificate | apps/api/src/modules/course/admin/certificate/* | Certificate templates |
| Customer discovery | apps/api/src/modules/course/customer/discovery/* | Course discovery |
| Customer learning | apps/api/src/modules/course/customer/learning/* | Lesson access |
| Customer quiz | apps/api/src/modules/course/customer/quiz/* | Quiz runtime |
| Customer certificate | apps/api/src/modules/course/customer/certificate/* | Certificate generation |
| Processing | apps/api/src/modules/course/processing/* | BullMQ processor |
| DB schema | packages/db/src/schema/course/* | Table definitions |
12. Environment Variables
| Variable | Default | Description |
|---|---|---|
CONTENT_ADMIN_RATE_LIMIT | 100 | Rate limit for admin content operations |
CONTENT_ADMIN_RATE_WINDOW_SECONDS | 60 | Rate limit window for admin operations |
CONTENT_CUSTOMER_ITEM_RATE_LIMIT | 30 | Rate limit for customer content item requests |
CONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS | 60 | Rate 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_SECONDS | 300 | Cache TTL for admin certificate views |
COURSE_ADMIN_QUIZ_CACHE_TTL_SECONDS | 300 | Cache TTL for admin quiz views |
COURSE_CUSTOMER_QUIZ_CACHE_TTL_SECONDS | 300 | Cache TTL for customer quiz detail |
BULL_DEFAULT_ATTEMPTS | 4 | Default BullMQ job retry attempts |
BULL_DEFAULT_BACKOFF_DELAY_MS | 2000 | Default 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
- Feature Guide: See Course - Feature List Section 6 (State Models) for course and lesson lifecycle diagrams.
- API Reference: See Course - API & Integration Guide Section 11 (Endpoint Reference + Payload Cheatsheet) for API contracts.