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
- Tenant consistency.
attemptCounterstarts 0; increments on each attempt started.- Idempotent creation on
(tenantId, userId, courseId, source.ref). - Cannot reactivate from terminal states — create new enrollment instead.
expiresAtoptional; if present, scheduler expires at that time.
4. Domain Events
enrollment.created.v1enrollment.completed.v1enrollment.expired.v1enrollment.revoked.v1enrollment.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)