Skip to main content

Domain Model

:::info Source Sourced from services/enrollment-service/DOMAIN_MODEL.md in the documentation repo. :::

1. Aggregates

Enrollment (root)

type EnrollmentId = Branded<string, 'EnrollmentId'>;

interface Enrollment {
id: EnrollmentId;
tenantId: TenantId;
userId: UserId;
courseId: CourseId;
courseVersionId: CourseVersionId;
source: { kind: 'assignment' | 'purchase' | 'manual' | 'self_signup'; ref: string };
state: 'active' | 'completed' | 'expired' | 'revoked';
enrolledAt: ISODate;
completedAt?: ISODate;
expiresAt?: ISODate;
lastAccessedAt?: ISODate;
attemptCounter: number;
metadata?: Record<string, JSONValue>;
}

Seat (if org license tracks consumption)

interface Seat {
licenseId: LicenseId;
userId: UserId;
consumedAt: ISODate;
}

2. State Machine

[new] → active
active → completed (via progress.completion.recorded.v1)
active → expired (expiresAt reached)
active → revoked (license revoked / admin action)
(all terminal)

3. Invariants

  1. Tenant consistency.
  2. attemptCounter starts 0; increments on each attempt started.
  3. Idempotent creation on (tenantId, userId, courseId, source.ref).
  4. Cannot reactivate from terminal states — create new enrollment instead.
  5. expiresAt optional; if present, scheduler expires at that time.

4. Domain Events

  • enrollment.created.v1
  • enrollment.completed.v1
  • enrollment.expired.v1
  • enrollment.revoked.v1
  • enrollment.accessed.v1 (on play session start)

5. Diagram

marketplace.license.granted.v1 ──▶ enrollment.create
assignment.window.opened.v1 ──▶ enrollment.create (on first play)
manual/self-signup ──▶ API ──▶ enrollment.create


emit enrollment.created.v1


content-service (trigger bundle creation)
progress-service (await first play)
notification-service (welcome)