ADR 0038 — schedule-events Topic: Partitioning, Retention, and the Outbox vs Best-Effort Split
Status: Accepted
Context date: 2026-08-06
Builds on: ADR 0024 (outbox pattern, platform-events), ADR 0025 (status projection), ADR 0030 (entitlement projection), ADR 0032 (service boundary)
Context
The scheduler emits lifecycle and run events for three consumers: the notification service (tenant subscription webhooks, email/in-app alerts), Analytics (long-horizon run history in ClickHouse), and the tenants themselves (via notification's fan-out). docs/06 sets the conventions — envelope, partition-key-per-ordering-scope, and outbox-for-business-critical — and this ADR extends them to a new topic. The design question with teeth: which events must survive a crash between DB commit and Kafka publish, and which may be lost?
Decision
1. Two new topics
| Topic | Partition key | Retention | Producers |
|---|---|---|---|
schedule-events | tenant_id | 14 d | Scheduler |
schedule-dlq | — | 14 d | Scheduler (exhausted deliveries) |
Partitioning by tenant_id gives per-tenant ordering, matching platform-events (ADR 0024) — the ordering scope consumers actually need (a run.succeeded never overtakes its run.started for one tenant). The envelope is pkg/events.Envelope unchanged; room_id is omitted. Schema: contracts/events/schedule-events.json.
2. Outbox vs best-effort, per event type
| Type | Path | Why |
|---|---|---|
schedule.created / updated / deleted | outbox | Lifecycle facts; consumers (notification, analytics) must never miss a mutation. |
schedule.paused / resumed | outbox | Same — and actor=system pauses are the audit trail of platform actions. |
schedule.triggered | outbox (product-critical) | For action=event schedules, this event is the product delivery — losing it between commit and publish loses the tenant's job. The same argument docs/06 makes for room.closed. |
schedule.run.failed | outbox | Drives tenant alerting through the ADR 0024/0025 machinery. |
schedule.disabled_auto | outbox | Breaker trips must reach tenant admins; carries admin_emails per the IDs-plus-data stance (ADR 0024/0025). |
schedule.run.started / succeeded / skipped | best-effort | High-volume observational telemetry; authoritative state is queryable via the run-history API, so a lost copy costs nothing but a dashboard blip. |
The outbox is the scheduler's own outbox table in scheduler_db, same shape and RelayOutbox (FOR UPDATE SKIP LOCKED) pattern as auth's (ADR 0024). Unlike the auth outbox (which ADR 0025 made a deliberate two-writer contract), the scheduler is the only writer of its outbox.
3. Consumed topics
| Topic | Group | Purpose |
|---|---|---|
platform-events | scheduler-tenant-status | tenant.suspended / activated / provisioned → tenant_status projection, occurred_at-guarded (ADR 0025 §3 verbatim). |
platform-events | scheduler-entitlements | ADR 0030 entitlement snapshot + quota-verdict projection (entitlement_version guard). |
Notification requires no code change to consume schedule-events — its dispatcher already matches on envelope type per tenant subscription; only its topic list grows.
Alternatives Considered
- Reusing
platform-events. Rejected: wrong audience and volume profile —platform-eventsis low-volume internal lifecycle with PII-driven short retention (ADR 0024); run events are high-volume and tenant-consumable. - Reusing
room-events. Rejected: wrong ordering scope (room_id) for events that have no room, and it would entangle two unrelated bounded contexts' retention and consumer groups. - Everything through the outbox. Rejected:
run.started/succeeded/skippedat full volume would make the outbox table and relay the service's hottest path for events whose loss is harmless — the run-history API is the authoritative record. - Everything best-effort. Rejected: loses
schedule.triggeredforaction=eventschedules on a crash — i.e., loses the product itself — and breaks the alerting chain forrun.failed/disabled_auto.
Consequences
- Positive: docs/06's conventions hold unmodified: one envelope, ordering scoped to what consumers need, outbox reserved for business-critical facts.
- Positive: Tenant alerting for failing/disabled schedules rides the proven ADR 0024/0025 notification machinery with zero new mechanism.
- Negative / accepted: Best-effort run events mean Analytics' ClickHouse history can have gaps that the run-history API does not; long-horizon analytics is explicitly a lossy mirror of
scheduler_db, not a second source of truth. - Negative / accepted:
admin_emailsonschedule.disabled_autocontinues the accepted PII-on-Kafka stance (ADR 0024/0025) — bounded here by the 14 d retention. - Follow-up: Declare both topics explicitly (retention, produce ACL restricted to the scheduler relay) rather than relying on broker auto-creation — the same standing follow-up ADR 0024/0025 carry for
platform-events.