Events Wiring
Recipes are working wiring examples with the sharp edges annotated. This page and the
events-wiringrecipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.
Two packages compose:
@modularityjs/events—EventBusabstract contract (dispatch<T extends DomainEvent>(event) → EventDispatchResult), the@OnEvent(EventClass, options)method decorator,EventListenersPoolfor listener registration, and theDomainEventinterface ({ readonly occurredAt: Date }).@modularityjs/events-{memory,redis}— drivers. Memory dispatches in-process, sequentially or in parallel per config. Redis pub/sub fans events across instances; needsRedisModule.
Define each event as a small class implementing DomainEvent. Pass the class to @OnEvent — the framework wires the dispatch by class identity (not by string). Listeners are regular @Injectable() services registered through EventListenersPool.
import { Inject, Injectable } from '@modularityjs/di';
import { inversify } from '@modularityjs/di-inversify';
import type { DomainEvent } from '@modularityjs/events';
import {
EventBus,
EventListenersPool,
EventsModule,
OnEvent,
} from '@modularityjs/events';
import { EventsMemoryModule } from '@modularityjs/events-memory';
import { createApp, Module, ModularityModule } from '@modularityjs/modularity';
class UserRegisteredEvent implements DomainEvent {
readonly occurredAt = new Date();
constructor(
readonly userId: string,
readonly email: string,
) {}
}
@Injectable()
class WelcomeEmailListener {
@OnEvent(UserRegisteredEvent, { name: 'send-welcome-email' })
async handle(event: UserRegisteredEvent): Promise<void> {
console.log(`Welcome ${event.email}, id ${event.userId}`);
}
}
@Injectable()
class RegistrationService {
constructor(@Inject(EventBus) private readonly events: EventBus) {}
async register(email: string): Promise<string> {
const userId = crypto.randomUUID();
await this.events.dispatch(new UserRegisteredEvent(userId, email));
return userId;
}
}
@Module({
name: 'registration',
imports: [EventsModule],
providers: [RegistrationService, WelcomeEmailListener],
pools: [
{
pool: EventListenersPool,
key: 'welcome-email',
useClass: WelcomeEmailListener,
},
],
})
class AppModule {}
const app = await createApp({
di: inversify,
modules: [ModularityModule, EventsModule, EventsMemoryModule, AppModule],
});@OnEvent options:
name(required) — stable identifier for the handler (used byonceto track which instance ran it across deployments).order— handler priority within an event; lower runs first.once: true— process exactly once across all instances. Requires Redis (it uses Redis as the coordination store); meaningless on the memory driver.local: true— only handlers in the dispatching instance run; events don't fan out to other nodes. Useful for in-process side effects (cache invalidation) that other nodes do for themselves.
once and local are mutually exclusive — combining them throws a ValidationException at decorator-time. local means "don't cross the bus", once means "exactly one instance handles it", which only makes sense when the event does cross the bus.
dispatch returns EventDispatchResult { ok, errors[] } — handler errors don't propagate. Check result.ok (or result.errors) when you need to know whether downstream handlers succeeded; otherwise the call is fire-and-forget.
Switching to Redis is EventsRedisModule + RedisModule.forRoot({ url }). RegistrationService and WelcomeEmailListener are unchanged.