Events
Events let you decouple side effects from your core business logic. Instead of calling a mailer, a queue, and an analytics tracker directly inside your service, you fire a single event and let listeners react to it independently.
This makes each piece easier to test, easier to change, and prevents your core logic from knowing about things it shouldn't care about.
Create an event
An event is a plain class that holds data for a moment in time. Generate one:
pearl make:event UserRegistered
# → src/events/UserRegisteredEvent.tsimport { Event } from '@pearl-framework/pearl'
// An event is a data carrier — no logic, no dependencies
export class UserRegisteredEvent extends Event {
constructor(
public readonly userId: number,
public readonly email: string,
) { super() }
}Create a listener
A listener extends Listener<T> and implements handle() for one specific event type:
pearl make:listener SendWelcomeEmail --event UserRegistered
# → src/listeners/SendWelcomeEmailListener.tsimport { Listener } from '@pearl-framework/pearl'
import { UserRegisteredEvent } from '../events/UserRegisteredEvent.js'
export class SendWelcomeEmailListener extends Listener<UserRegisteredEvent> {
async handle(event: UserRegisteredEvent): Promise<void> {
const job = new SendWelcomeEmailJob()
job.userId = event.userId
await queue.dispatch(job)
}
// Optional — return false to skip this listener silently
shouldHandle(event: UserRegisteredEvent): boolean {
return event.userId > 0
}
}Register listeners
Wire events to listeners in your service provider using dispatcher.on(EventClass, ListenerClass). You can attach multiple listeners to the same event.
import { EventDispatcher } from '@pearl-framework/pearl'
import { UserRegisteredEvent } from '../events/UserRegisteredEvent.js'
import { SendWelcomeEmailListener } from '../listeners/SendWelcomeEmailListener.js'
import { NotifyAdminsListener } from '../listeners/NotifyAdminsListener.js'
// Inside register():
this.container.singleton(EventDispatcher, () => {
const dispatcher = new EventDispatcher()
dispatcher.on(UserRegisteredEvent, SendWelcomeEmailListener)
dispatcher.on(UserRegisteredEvent, NotifyAdminsListener) // multiple listeners ok
return dispatcher
})Dispatch an event
Inject EventDispatcher into your service and call dispatch(). All registered listeners run and are awaited:
const dispatcher = app.container.make(EventDispatcher)
// Dispatches and awaits all listeners
await dispatcher.dispatch(new UserRegisteredEvent(user.id, user.email))
// Fire-and-forget — does not await listeners
dispatcher.dispatchSync(new UserRegisteredEvent(user.id, user.email))Why use events?
Without events, your registration handler is tightly coupled to every side effect:
// Without events — hard to test, hard to change
await sendWelcomeEmail(user)
await notifyAdmins(user)
await trackAnalytics('user.registered', user.id)With events, it becomes one line — and each side effect is independently testable:
// With events — clean, decoupled, easy to extend
await dispatcher.dispatch(new UserRegisteredEvent(user.id, user.email))