Events
:::info Source
Sourced from services/catalog-service/EVENT_SCHEMAS.md in the documentation repo.
:::
Companion: ../../docs/04-event-driven-architecture.md · APPLICATION_LOGIC
All events use the platform envelope defined in 04 §4 and register their payload schema at schemas://catalog/{aggregate}/{event}/v{N}.
1. Streams & Subjects
| Stream | Subjects | Retention |
|---|---|---|
CATALOG | catalog.> | operational: 30 d hot, 13 mo cold |
CATALOG.dlq | catalog.>.dlq | 90 d |
Partition key on all published events: courseId (or taxonomyId).
2. Published Events
2.1 catalog.course.registered.v1
Trigger: RegisterCourse use case (first publish of a slug within tenant).
{
"eventId": "01HYZ…",
"eventType": "catalog.course.registered",
"eventVersion": 1,
"schemaUri": "schemas://catalog/course/registered/v1#sha256-ab…",
"source": { "service": "catalog-service", "instance": "catalog-7f4c", "commit": "a1b2c3" },
"occurredAt": "2026-03-02T09:04:01Z",
"ingestedAt": "2026-03-02T09:04:01Z",
"correlationId": "01HYZ…",
"causationId": "01HYZ…",
"tenantId": "ten_01H…",
"actor": { "type": "system", "id": "catalog-service" },
"partitionKey": "crs_01H…",
"retentionClass": "operational",
"dataResidency": "us",
"payload": {
"courseId": "crs_01H…",
"slug": "intro-physics",
"title": { "en": "Intro Physics" },
"defaultLocale": "en",
"visibility": "org",
"authors": [{ "userId": "usr_01H…", "role": "author" }],
"taxonomy": [{ "taxonomyId": "tax_01H…", "nodePath": "/science/physics" }],
"sourceDraftId": "drf_01H…"
}
}
Consumers: search-service, notification-service, analytics-service.
2.2 catalog.course.metadata_updated.v1
{
"payload": {
"courseId": "crs_01H…",
"changedFields": ["title", "description", "cover"],
"previous": { "title": { "en": "old" } },
"next": { "title": { "en": "new" } },
"etag": "01HYZ…"
}
}
2.3 catalog.course.visibility_changed.v1
{ "payload": { "courseId": "crs_01H…", "from": "org", "to": "marketplace", "reason": "Launch Q2" } }
2.4 catalog.course.archived.v1
{ "payload": { "courseId": "crs_01H…", "reason": "Deprecated product line" } }
2.5 catalog.course_version.published.v1
Trigger: PublishCourseVersion use case, after content.play_package.built.v1.
{
"payload": {
"courseVersionId": "crv_01H…",
"courseId": "crs_01H…",
"versionLabel": "2.1.0",
"publishedBy": "usr_01H…",
"durationMinutes": 420,
"estimatedReadingMinutes": 380,
"locales": ["en", "ar"],
"moduleSummaries": [
{ "id": "mod_1", "title": { "en": "Kinematics" }, "lessonCount": 6, "durationMinutes": 60, "hasAssessments": true }
],
"playPackage": {
"playPackageId": "pkg_01H…",
"sha256": "a3f1…",
"format": "v1"
},
"becameLatest": true,
"changelog": { "en": "Fixed typos in module 2." }
}
}
Consumers: search-service (index), marketplace-service (refresh listing), enrollment-service (stamp catalog ref), delivery-service (prewarm), notification-service (author + learners of prior version), analytics-service.
2.6 catalog.course_version.deprecated.v1
{ "payload": { "courseVersionId": "crv_01H…", "courseId": "crs_01H…", "reason": "Superseded by 3.0.0" } }
2.7 catalog.course_version.withdrawn.v1
{
"payload": {
"courseVersionId": "crv_01H…",
"courseId": "crs_01H…",
"reason": "Legal complaint: copyright",
"affectedEnrollmentsApprox": 1230
}
}
reason is REQUIRED.
2.8 catalog.taxonomy.updated.v1
{
"payload": {
"taxonomyId": "tax_01H…",
"namespace": "acme:role_ladder",
"version": 14,
"diff": {
"added": [{ "path": "/ic/l6", "label": { "en": "L6" } }],
"removed": [],
"moved": [],
"renamed": []
}
}
}
3. Consumed Events
3.1 content.play_package.built.v1
Handler: PublishCourseVersion.
Expected payload fields used by catalog:
{
"payload": {
"courseDraftId": "drf_01H…",
"tenantId": "ten_01H…",
"slug": "intro-physics",
"title": { "en": "…" },
"description": { "en": "…" },
"defaultLocale": "en",
"authors": [{ "userId": "usr_01H…", "role": "author" }],
"taxonomy": [{ "taxonomyId": "tax_01H…", "nodePath": "/science/physics" }],
"tags": ["physics"],
"cover": { "assetId": "med_01H…" },
"versionLabel": "2.1.0",
"durationMinutes": 420,
"locales": ["en", "ar"],
"moduleSummaries": [ "…" ],
"playPackage": { "playPackageId": "pkg_01H…", "sha256": "…", "format": "v1" }
}
}
Durable consumer: catalog-svc-play-package-built, ack-wait 30 s, max redeliver 5.
3.2 authoring.course_draft.published.v1
Handler: UpsertCourseMetadataFromDraft (creates Course if missing, else updates mutable metadata).
{
"payload": {
"courseDraftId": "drf_01H…",
"tenantId": "ten_01H…",
"slug": "intro-physics",
"defaultLocale": "en",
"metadata": { "title": { "en": "…" }, "description": { "en": "…" }, "authors": [], "taxonomy": [] }
}
}
Note: the version is not created from this event. Catalog waits for content.play_package.built.v1 to create the CourseVersion. This keeps the saga ordering deterministic.
3.3 gdpr.subject_request.received.v1
Handler: HandleGdprRequest — for subject type user:
- If user appears in any
authors[], replacedisplayNamewith"[redacted]"; keepuserId. - Emit
catalog.course.metadata_updated.v1withchangedFields: ['authors']. - For
erasurerequests,authors[].userIdis zeroed tousr_00000…only ifretentionClasspermits.
4. JSON Schema (illustrative)
schemas://catalog/course_version/published/v1.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schemas://catalog/course_version/published/v1.json",
"type": "object",
"required": ["courseVersionId","courseId","versionLabel","publishedBy","durationMinutes","locales","playPackage"],
"properties": {
"courseVersionId": { "type":"string", "pattern":"^crv_[0-9A-HJKMNP-TV-Z]{26}$" },
"courseId": { "type":"string", "pattern":"^crs_[0-9A-HJKMNP-TV-Z]{26}$" },
"versionLabel": { "type":"string", "pattern":"^\\d+\\.\\d+\\.\\d+$" },
"publishedBy": { "type":"string", "pattern":"^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
"durationMinutes": { "type":"integer", "minimum": 0 },
"estimatedReadingMinutes": { "type":"integer", "minimum": 0 },
"locales": { "type":"array", "items":{ "type":"string" }, "minItems":1 },
"moduleSummaries": { "type":"array", "items": { "$ref": "#/$defs/moduleSummary" } },
"playPackage": { "$ref": "#/$defs/playPackage" },
"becameLatest": { "type":"boolean" },
"changelog": { "type":"object", "additionalProperties": { "type":"string" } }
},
"$defs": {
"moduleSummary": { "…": "…" },
"playPackage": {
"type":"object",
"required": ["playPackageId","sha256","format"],
"properties": {
"playPackageId": { "type":"string", "pattern":"^pkg_[0-9A-HJKMNP-TV-Z]{26}$" },
"sha256": { "type":"string", "pattern":"^[a-f0-9]{64}$" },
"format": { "type":"string", "enum": ["v1","v2"] }
}
}
}
}
5. Compatibility & Evolution
| Change | Treatment |
|---|---|
| Add optional field | Minor; safe in v1 |
| Add required field | Major; requires v2 + dual-publish |
| Rename / retype field | Major |
| Remove field | Major |
| Enum narrowing | Major |
| Enum widening | Minor; consumers must tolerate unknown enum values |
Dual-publish window: 30 days. Platform schema registry rejects breaking changes without a new version.
6. Ordering & Partitioning
- FIFO per
partitionKey(=courseIdortaxonomyId). - Readers MUST treat events for different keys as independent.
catalog.course_version.published.v1events for the same course are ordered and never reordered.
7. Replay Safety
- All handlers are idempotent via inbox
eventIdPK. - Outbox ensures at-least-once publish; NATS
Nats-Msg-Idprovides dedup. - Projectors (search, marketplace) MUST be idempotent using their own inbox.
8. DLQ & Poison Messages
- Failure after 5 redelivers →
CATALOG.dlq.catalog.*. - Metadata:
x-original-subject,x-error,x-stack,x-retries. - Ops dashboard shows DLQ depth; alert at > 0 for > 10 min.
- Replay path:
POST /admin/v1/catalog/dlq/replaywith filter.
9. Event Version History
| Event | v1 released | Breaking changes since |
|---|---|---|
| all | 2026-Q1 | none |