Domain Events
This page documents the current event architecture in the API, following the same style as the rest of the module docs: module-owned events, application-level publishing, adapter subscribers, and composition-time wiring.
Purpose
The events architecture:
- keeps event ownership in each module domain (
domain/events) - publishes from use-cases through an application port (
DomainEventPublisherPort) - wires concrete subscribers in module composition
- uses shared in-process event bus primitives from
shared/kernel/event-bus.ts - allows infrastructure side effects (logging, SQS) without coupling use-cases to infra SDKs
Where events are defined
Current users module events are defined in:
apps/api/src/modules/users/domain/events/user-events.ts
Event union: UsersDomainEvent
Current event types:
users.user-registeredusers.user-profile-updatedusers.user-interests-setusers.user-soft-deletedusers.user-interests-read
Each event has:
type: discriminated string event nameoccurredAt: ISO timestamppayload: typed business data
Where events are published
Publishing is done in application use-cases via DomainEventPublisherPort (apps/api/src/modules/users/application/ports/domain-event-publisher.port.ts).
Current publishers:
register-user-by-identity-provider-id.ts-> publishesusers.user-registeredupdate-profile.ts-> publishes pulled domain events fromuser.entityset-user-interests.ts-> publishes pulled domain events fromuser.entitysoft-delete-by-user-id.ts-> publishes pulled domain events when soft delete succeedsget-user-interests.ts-> publishesusers.user-interests-readwhen data is found
createNoopDomainEventPublisher() is used as default dependency for use-cases, so use-cases stay runnable in tests without wiring full subscribers.
Composition and wiring
Wiring is centralized in apps/api/src/modules/users/composition.ts.
Composition creates:
- shared log handler (
createLogDomainEventHandler) - welcome-email subscriber (
createPublishWelcomeEmailOnUserRegisteredSubscriber) - typed event bus publisher (
createEventBusPublisher<UsersDomainEvent>) handlersByTypemapping for each event type- use-cases injected with
domainEventPublisher
This keeps use-cases unaware of SQS/logging details; they only call publish(event).
Event bus behavior
Shared implementation: apps/api/src/shared/kernel/event-bus.ts
createEventBusPublisher({ handlersByType }) behavior:
- selects handlers by
event.type - falls back to empty handler list when none are registered
- executes handlers in parallel with
Promise.all
Because handlers run in parallel, subscribers should be idempotent and safe for concurrent execution.
Current subscribers
1) Logging subscriber
- factory:
apps/api/src/shared/kernel/log-domain-event.handler.ts - wired for all current users events in users composition
- logs
{ event, scope }via shared logger
2) Welcome email SQS subscriber
- implementation:
apps/api/src/modules/users/adapters/events/subscribers/publish-welcome-email-on-user-registered.subscriber.ts - listens only to
users.user-registered - sends SQS message only when
event.payload.isNewUser === true - sends message with:
kind: "welcome-email-request"version: 1- payload:
{ userId, identityProviderUserId } - meta:
{ source, occurredAt }
- wraps SQS errors with
createSqsOperationError(...)
Runtime flow
- HTTP route/handler calls a use-case.
- Use-case performs domain + repository operations.
- Use-case publishes one or more domain events.
- Event bus routes by
event.type. - Subscribers run (log, SQS, etc.).
Adding a new event (current style)
- Add typed event in module
domain/events/*and include it in module event union. - Publish from the correct use-case (or from domain entity ->
pullEvents()consumed by use-case). - Implement subscribers in adapters (
adapters/events/subscribers/*) when side effects are needed. - Wire handlers in
composition.tsunderhandlersByType. - Keep handlers idempotent and failure-aware (wrap infra errors with shared kernel error helpers).