Appearance
Integration Events Publisher/Consumer Pattern
Status
Active
Context
The notifications module was importing UserRegisteredEvent and ClientRegisteredEvent directly from identity/domain/events/. Domain events are internal to a bounded context. This cross-module coupling:
- Prevents
identityfrom ever being extracted into a microservice without also modifyingnotifications. - Violates the module self-containment principle established in ADR 004.
- Places cross-module transport logic inside the domain layer, which should contain only business rules.
Additionally, the event handlers lived under notifications/application/events/, a conceptually incorrect location — they were reacting to another module's internals, not executing application logic internal to notifications.
Decision
Introduce a three-layer decoupling:
Shared integration events — plain data contracts in
shared/integration-events/identity/. These are the bounded-context boundary: identity publishes facts, notifications subscribes to them. Neither module owns these contracts; they live inshared/and can be extracted to a workspace package when needed.IdentityPublisher— a service inidentity/presentation/publishers/. The presentation layer is the right place for cross-context publishing: it faces outward, the application layer faces inward. The publisher wrapsEventBusand exposes typedpublishXxx()methods. Callers (RegisterUserHandler,RegisterClientHandler) injectIdentityPublisherinstead ofEventBusdirectly.NotificationsConsumers—@EventsHandlerclasses innotifications/presentation/consumers/. The presentation layer of notifications is the right place for inbound integration: it faces outward and translates external facts into internal commands (SendEmailNotificationCommand). Template selection stays private to notifications.Base observability wrappers — shared tracing and metadata plumbing for this boundary is implemented in
shared/infrastructure/observability/(BasePublisherandBaseConsumer), then extended by concrete identity publisher and notifications consumers. This keeps the contract behavior identical while removing duplicated span/metadata code.
Consequences
Positive
- No cross-module imports between
identityandnotifications. identitycan be extracted as a microservice by swappingEventBus.publish()with a Kafka emit inIdentityPublisher— no other identity code changes.notificationscan be converted to a Kafka consumer by replacing@EventsHandlerwith a Kafka listener decorator — no internal notification logic changes.- Template keys remain private to
notifications; identity only publishes facts. fire-and-forgetsemantics preserved:publishXxx()isvoid,EventBus.publish()is synchronous dispatch.
Neutral
shared/integration-events/is a new shared boundary layer. It must be kept minimal — only plain data objects, no framework decorators.
Negative
- One additional indirection layer (
IdentityPublisher) vs. callingEventBusdirectly in the command handler.
File Map
| Role | Path |
|---|---|
| Integration event contracts | shared/integration-events/identity/ |
| Publisher | modules/identity/presentation/publishers/identity.publisher.ts |
| Consumers | modules/notifications/presentation/consumers/ |
Deleted Files
| Path | Reason |
|---|---|
modules/identity/domain/events/user-registered.event.ts | Replaced by UserRegisteredIntegrationEvent in shared/ |
modules/identity/domain/events/client-registered.event.ts | Replaced by ClientRegisteredIntegrationEvent in shared/ |
modules/notifications/application/events/user-registered-notification.handler.ts | Moved to presentation/consumers/ |
modules/notifications/application/events/client-registered-notification.handler.ts | Moved to presentation/consumers/ |
modules/notifications/application/events/index.ts | Folder emptied |
Path to Microservices
Only these two files need to change per extraction:
IdentityPublisher.publishXxx()→ emit to Kafka topic instead ofEventBus.publish()UserRegisteredConsumer/ClientRegisteredConsumer→ listen to Kafka topic instead of@EventsHandler
All internal module logic, domain entities, use cases, and the shared contract classes remain unchanged.