Training Module Backend Documentation
Internal architecture, data model, cache contracts, rate limits, and queue/runtime behavior for training enrollment.
Training - Backend Documentation
Audience: Backend developers and system integrators Scope: Admin + customer/mobile training surfaces, schema, cache, rate limit, order integration, and worker/runtime contracts.
1. Backend Scope and Boundaries
Training backend is a feature-structured NestJS domain with three surfaces:
- Admin surface: training, session, cohort, waitlist, and enrollment administration
- Customer/mobile surface: enrollment request, enrollment detail/list, session access, cancellation request
- Internal async/runtime surface: order integration + BullMQ training worker contracts
Primary boundaries:
- Commerce dependency: training enrollments are tied to
productrows withproductType = "training" - Cart bypass: training products are intentionally excluded from cart and use dedicated enrollment flow
- Order ownership: enrollment request creates training order + payment initiation synchronously; payment success finalizes enrollment/access
- Shared entitlement:
product_accessis upserted for paid training completion (same entitlement table used by other digital products)
2. Module Composition (Aggregate + Leaf)
2.1 Training module layout
TrainingModule- shared/helper provider:
TrainingSharedHelperService - imports:
DatabaseModule,RedisModule,OrderModule,ProductAccessModule,TrainingProcessorModule
- shared/helper provider:
TrainingAdminAggregateModule- leaf modules:
TrainingAdminModuleSessionAdminModuleCohortAdminModuleTrainingEnrollmentAdminModule
- leaf modules:
TrainingCustomerAggregateModule- leaf modules:
TrainingCustomerModuleProductAccessModule
- leaf modules:
TrainingProcessorModule- BullMQ queue registration:
QueueName.TRAINING - processor:
TrainingProcessor - provides
TrainingSharedHelperServicedirectly to avoid circular module import withTrainingModule
- BullMQ queue registration:
2.2 API route ownership
| Surface | Prefix | Controller | Swagger Tag |
|---|---|---|---|
| Admin training | /api/admin/training | TrainingAdminController | Training (Admin) |
| Admin sessions | /api/admin/training/:trainingId/sessions | SessionAdminController | Training Session (Admin) |
| Admin cohorts | /api/admin/training/:trainingId/cohorts | CohortAdminController | Training Cohort (Admin) |
| Admin enrollments | /api/admin/training/enrollments | TrainingEnrollmentAdminController | Training Enrollment (Admin) |
| Customer/mobile | /api/training and /api/mobile/training | TrainingCustomerController | Training Enrollment (Mobile) |
2.3 Mobile composition
mobile.module.ts mounts TrainingCustomerAggregateModule under path: "mobile" via RouterModule.register(...). Customer controller path stays @Controller("training"), so mobile-prefixed routes are produced without embedding mobile in controller paths.
3. Data Model (Drizzle / PostgreSQL)
Training schema source of truth: packages/db/src/schema/training/*.
3.1 Core tables
| Table | Purpose | Notable constraints/indexes |
|---|---|---|
training | Training metadata linked to product | unique product_id, indexes on category_id, status; title is sourced from products.title via product_id |
training_session | Session units for a training product | indexes on training_id, status, session_order |
training_cohort | Capacity-based batch/cohort | unique (training_id, cohort_name), checks on max_seats > 0, enrolled_count >= 0 |
training_cohort_session | Cohort-session join with schedule override | composite PK (cohort_id, session_id) |
training_enrollment_form_config | Optional per-training form schema | unique training_id |
training_waitlist | Overflow queue for full cohorts | unique (user_id, cohort_id), indexes on FK columns |
training_enrollment | User enrollment state and access metadata | indexes on user_id, product_id, order_id, cohort_id, session_id, status; partial unique index training_enrollment_enrolled_unique on (user_id, product_id) where status='enrolled' |
3.2 Enums
| Enum | Values |
|---|---|
training_status | draft, published, hidden, archived |
training_session_type | recorded, live, both |
training_session_status | scheduled, live, completed, cancelled |
training_cohort_status | open, closed, full, cancelled |
training_waitlist_status | waiting, promoted, expired, cancelled |
training_access_type | lifetime, time_limited |
training_enrollment_status | pending, enrolled, cancelled |
3.3 Referential integrity highlights
training.product_id -> products.id(cascade)training_session.training_id -> products.id(cascade)training_cohort.training_id -> products.id(cascade)training_waitlist.user_id -> customers.id(cascade)training_waitlist.cohort_id -> training_cohort.id(cascade)training_enrollment.user_id -> customers.id(cascade)training_enrollment.product_id -> products.id(cascade)training_enrollment.order_id -> orders.id(set null)training_enrollment.cohort_id -> training_cohort.id(set null)training_enrollment.session_id -> training_session.id(set null)
4. Runtime Rules and Domain Invariants
4.1 Product and training validation
TrainingSharedHelperService.assertTrainingProduct(...) enforces:
- product exists
productType === "training"- product is sellable and published
4.2 Enrollment invariants
TrainingCustomerService.enroll(...) enforces:
- no active duplicate enrollment for
(userId, productId)except reusable pending row - optional cohort/session must belong to training
- required form fields must satisfy configured
formSchema.fields[*].required - advisory lock is used for concurrent same-user/product enrollment writes
- database enforces one terminal enrolled row per
(user_id, product_id)via partial unique index - pending lifecycle remains compatible with
order_id IS NULL(nonum_nonnullsenforcement on pending rows)
4.3 Status transitions and guardrails
- Training status transition map:
draft -> publishedpublished -> hiddenhidden -> publishedarchived -> (none)
- Cohort status transition map:
open -> closed/full/cancelledclosed -> open/cancelledfull -> open/cancelledcancelled -> (none)
- Customer cancellation:
pendingcancellation rejectedenrolled -> cancelledallowed- already
cancelledis idempotent
4.4 Capacity and seat-count adjustments
- Cohort
enrolledCountincrements when enrollment becomesenrolled - For payment-success transitions, increment happens in order processor while promoting pending enrollment to enrolled.
- Cohort
enrolledCountdecrements on cancellation (bounded withgreatest(..., 0)) - Cohort/session assignment validates all session IDs belong to same training and disallows duplicate session IDs in payload
4.5 Session access invariants
For GET /training/:trainingId/sessions/:sessionId/content:
- user must have
enrolledtraining enrollment - session must belong to training
- access resolution order:
- recording available + valid recording URL ->
accessibleVia = "recording" - else if session status is
live->accessibleVia = "live" - else throw domain error (
TRAINING_SESSION_NOT_LIVEorTRAINING_RECORDING_NOT_AVAILABLE)
- recording available + valid recording URL ->
5. Caching Strategy
5.1 Keyspaces
| Key prefix | Used by |
|---|---|
training:admin:list: | Admin training list |
training:admin:detail: | Admin training detail |
training:session:admin:list: | Admin session list by training |
training:session:customer:list: | Customer accessible sessions |
training:cohort:admin:list: | Admin cohort list |
training:cohort:waitlist: | Cohort waitlist list |
training:enrollment:list: | Customer enrollment list |
training:enrollment:detail: | Customer enrollment detail |
training:enrollment:admin:list: | Admin enrollment list |
training:enrollment:admin:detail: | Admin enrollment detail |
training:product-access:user: | Product access rows by user |
All keys are deterministic via CacheKeyUtil.build(...) and segment normalization.
5.2 Invalidation patterns
Invalidation is prefix-based via invalidatePattern(prefix*).
Common invalidations:
- enrollment writes -> enrollment list/detail + customer session list keyspaces
- session writes -> training session admin list keyspace
- cohort writes -> cohort list and waitlist keyspaces
- payment success -> enrollment + product-access keyspaces (order processor)
5.3 TTL
- General training cache TTL is
REDIS_CACHE_TTL_SECONDS. - Redis failures degrade gracefully: services log warning and continue with DB path.
6. Rate Limiting Strategy
Controllers use Redis sliding-window ZSET logic:
- remove expired members
- add request marker (UUIDv7 or timestamp+UUIDv7)
- count window
- set key expiry
- reject with
RateLimitExceededExceptionwhen threshold exceeded
Rate-limit env/config:
- admin controllers:
TRAINING_ADMIN_RATE_LIMIT,TRAINING_ADMIN_RATE_WINDOW_SECONDS - customer controller:
ORDER_CHECKOUT_RATE_LIMIT,ORDER_CHECKOUT_RATE_WINDOW_SECONDS - discovery/customer-content path:
CONTENT_CUSTOMER_ITEM_RATE_LIMIT,CONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS
Rate-limit key prefixes:
- admin:
rl:training:admin: - customer:
rl:training:customer:
If Redis rate-limit check fails, request is allowed and warning is logged (graceful degradation).
7. Order and Queue Runtime
7.1 Primary enrollment runtime (current production path)
TrainingCustomerService.enroll(...) creates training artifacts synchronously:
- pending
training_enrollmentrow (new or reused pending enrollment) - training
orderrow (orderType = "training",paymentStatus = "pending",orderStatus = "payment_pending") - pending
paymentrecord + payment initiation payload async_requestsrow (scope = "training_enrollment") for deterministic idempotency replay- unpaid-order cancellation window:
ORDER_AUTO_CANCEL_MINUTES(default30)
After redirect verification, order jobs finalize payment state. On payment success, OrderProcessor upserts training enrollment access fields plus product access.
Training enrollment finalization is owned by this order-processor path so seat-count and access updates stay in one runtime flow.
7.2 Training queue contract (internal worker surface)
packages/jobs defines queue/job contracts:
- queue:
QueueName.TRAINING("training") - jobs:
TrainingJob.CREATE_ENROLLMENTTrainingJob.CANCEL_ENROLLMENT
Payloads:
CreateTrainingEnrollmentPayload:requestId,correlationId,userId,productId,cohortId,sessionId,formData,paymentMethod
CancelTrainingEnrollmentPayload:requestId,correlationId,enrollmentId,userId,reason?,cancelledBy
7.2.1 Outbox cleanup relationship
Training payment and lifecycle notifications that are persisted through shared outbox_events are cleaned by the order maintenance cleanup job.
Retention behavior:
completedoutbox rows older thanOUTBOX_RETENTION_DAYSare deleted.failedoutbox rows older thanOUTBOX_FAILED_RETENTION_DAYSare deleted.pendingrows are never deleted by cleanup.
This retention behavior is implemented in order maintenance runtime (orders_maintenance queue) for shared outbox rows that use standard terminal statuses (completed/failed). Dispatch ownership remains target-queue scoped.
7.3 Training processor behavior
TrainingProcessor logs with [start], [success], [skip], [failure] patterns.
processCreateEnrollment:- calls
OrderService.createTrainingOrder(...) - returns
{ success, orderId }
- calls
processCancelEnrollment:- idempotency check against completed
async_requests - advisory lock on enrollment ID
- validates ownership for user-initiated cancellation
- updates enrollment + cohort seat count in one transaction
- marks
async_requestscompleted when request id is present
- idempotency check against completed
8. Error Contracts (Training)
TRAINING_* error codes from common/types/error-codes.ts:
| Code | Meaning |
|---|---|
TRAINING_ENROLLMENT_NOT_FOUND | Enrollment row not found |
TRAINING_PRODUCT_REQUIRED | Product type is not training |
TRAINING_PRODUCT_UNAVAILABLE | Training product not sellable/published |
TRAINING_NOT_FOUND | Training row not found |
TRAINING_SLUG_DUPLICATE | Slug conflict |
TRAINING_FORM_CONFIG_NOT_FOUND | Missing form config |
TRAINING_PRODUCT_CREATION_FAILED | Product/training creation failed |
TRAINING_INVALID_STATUS_TRANSITION | Invalid training lifecycle transition |
TRAINING_SESSION_NOT_FOUND | Session row not found |
TRAINING_SESSION_MISMATCH | Session does not belong to training |
TRAINING_SESSION_CREATE_FAILED | Session creation failed |
TRAINING_SESSION_DELETE_FAILED | Session delete failed |
TRAINING_SESSION_NOT_LIVE | Live session access attempted before live state |
TRAINING_RECORDING_NOT_AVAILABLE | Recording not available |
TRAINING_ALREADY_ENROLLED | Duplicate active enrollment |
TRAINING_INVALID_FORM_DATA | Required form data missing/invalid |
TRAINING_ENROLLMENT_CANCEL_NOT_ALLOWED | Cancellation forbidden for current state |
TRAINING_COHORT_NOT_FOUND | Cohort row not found |
TRAINING_COHORT_MISMATCH | Cohort does not belong to training |
TRAINING_COHORT_CREATE_FAILED | Cohort creation failed |
TRAINING_COHORT_DUPLICATE_NAME | Duplicate cohort name within training |
TRAINING_COHORT_INVALID_STATUS_TRANSITION | Invalid cohort status transition |
TRAINING_COHORT_CANCELLED | Cohort already cancelled |
TRAINING_COHORT_CLOSED | Cohort closed for enrollment |
TRAINING_COHORT_FULL | Cohort full/no seat |
TRAINING_COHORT_CAPACITY_EXCEEDED | maxSeats lower than enrolledCount |
TRAINING_WAITLIST_NOT_FOUND | Waitlist row not found |
TRAINING_WAITLIST_MISMATCH | Waitlist row not in cohort |
TRAINING_WAITLIST_DUPLICATE | Duplicate waitlist entry |
TRAINING_COHORT_SESSION_ASSIGN_FAILED | Session assignment payload/state invalid |
Related cross-module training flow errors:
CART_TRAINING_ENROLLMENT_FLOWORDER_TRAINING_FLOW_REQUIRED
9. Performance and Resilience Notes
- Enrollment, cancellation, and order artifact updates use DB transactions for atomicity.
- Enrollment and cancellation paths use advisory locks to prevent conflicting concurrent writes.
- Expensive list/detail endpoints are cache-aside with deterministic keys.
- Query projections are explicit; no
SELECT *patterns in training services. - Independent reads in admin enrollment detail use
Promise.all(order + user + training metadata).
10. Backend Diagrams
10.1 Admin composition
10.2 Enrollment runtime (customer -> order -> artifact)
10.3 Cancellation runtime
11. File Map
| Concern | File Path | Purpose |
|---|---|---|
| Admin training | apps/api/src/modules/training/admin/* | Training CRUD |
| Admin session | apps/api/src/modules/training/admin/session/* | Session management |
| Admin cohort | apps/api/src/modules/training/admin/cohort/* | Cohort management |
| Admin enrollment | apps/api/src/modules/training/admin/enrollment/* | Enrollment management |
| Customer enrollment | apps/api/src/modules/training/customer/* | Enrollment requests |
| Processing | apps/api/src/modules/training/processing/* | BullMQ processor |
| DB schema | packages/db/src/schema/training/* | Table definitions |
12. Environment Variables
| Variable | Default | Description |
|---|---|---|
TRAINING_ADMIN_RATE_LIMIT | - | Rate limit for admin training operations |
TRAINING_ADMIN_RATE_WINDOW_SECONDS | - | Rate limit window for admin training |
ORDER_CHECKOUT_RATE_LIMIT | 10 | Rate limit for checkout requests |
ORDER_CHECKOUT_RATE_WINDOW_SECONDS | 60 | Rate limit window for checkout |
CONTENT_CUSTOMER_ITEM_RATE_LIMIT | 30 | Rate limit for customer content access |
CONTENT_CUSTOMER_ITEM_RATE_WINDOW_SECONDS | 60 | Rate limit window for content access |
ORDER_AUTO_CANCEL_MINUTES | 30 | Minutes before unpaid orders are auto-cancelled |
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 Training - Feature List Section 6 (State Models) for training lifecycle diagram.
- API Reference: See Training - API & Integration Guide Section 11 (Endpoint Reference + Payload Cheatsheet) for API contracts.