Skip to main content

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

StreamSubjectsRetention
CATALOGcatalog.>operational: 30 d hot, 13 mo cold
CATALOG.dlqcatalog.>.dlq90 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[], replace displayName with "[redacted]"; keep userId.
  • Emit catalog.course.metadata_updated.v1 with changedFields: ['authors'].
  • For erasure requests, authors[].userId is zeroed to usr_00000… only if retentionClass permits.

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

ChangeTreatment
Add optional fieldMinor; safe in v1
Add required fieldMajor; requires v2 + dual-publish
Rename / retype fieldMajor
Remove fieldMajor
Enum narrowingMajor
Enum wideningMinor; 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 (= courseId or taxonomyId).
  • Readers MUST treat events for different keys as independent.
  • catalog.course_version.published.v1 events for the same course are ordered and never reordered.

7. Replay Safety

  • All handlers are idempotent via inbox eventId PK.
  • Outbox ensures at-least-once publish; NATS Nats-Msg-Id provides 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/replay with filter.

9. Event Version History

Eventv1 releasedBreaking changes since
all2026-Q1none