Skip to content

Event Listeners

An Event Listener is the simplest way to react to events in your system. While Projections build read models and Process Managers orchestrate complex workflows, Event Listeners are ideal for lightweight, stateless side effects.

Common use cases include:

  • Invalidating caches when data changes.
  • Sending notifications to external systems.
  • Logging or monitoring event flow.

Event Listeners are created via eventStore.createEventListener(). Unlike Projections and Process Managers, Event Listeners are not checkpointed — they don’t track which events they’ve seen. They only react to events as they flow through the system in real time.

Here is an event listener used for cache invalidation:

import eventStore from './eventStore'
import cache from './cache'
eventStore
.createEventListener('projectCache')
.withAfterEffects({
afterProjectUpdated({ streamId }) {
cache.invalidate(['project', streamId])
},
afterProjectDeleted({ streamId }) {
cache.invalidate(['project', streamId])
},
})

Event Listeners support four types of handlers, giving you fine-grained control over when your side effects run:

Listen to specific event types. These run during the main event processing phase, alongside projections and process managers:

eventStore
.createEventListener('notifications')
.withEventHandlers({
async onProjectCreated({ payload, actorId }) {
await sendSlackNotification(
`Project "${payload.name}" was created by ${actorId}`
)
},
})

Listen to specific event types, but run after all event handlers (across all listeners, projections, and process managers) have completed. Use the after<EventName> naming convention:

eventStore
.createEventListener('cacheInvalidation')
.withAfterEffects({
afterProjectUpdated({ streamId }) {
cache.invalidate(['project', streamId])
},
afterProjectDeleted({ streamId }) {
cache.invalidate(['project', streamId])
},
})

A catch-all handler that runs for every event. Useful for logging or monitoring:

eventStore
.createEventListener('systemLogger')
.withGlobalEventHandler((event) => {
console.log(`[Event] ${event.type} for stream ${event.streamId}`)
})

A catch-all handler that runs for every event after all other handlers have completed:

eventStore
.createEventListener('analytics')
.withGlobalAfterEffect(async ({ type, streamId, actorId }) => {
await trackEvent(type, { streamId, actorId })
})

You can chain multiple handler types on the same listener. This is a common pattern — specific after-effects combined with a global catch-all:

eventStore
.createEventListener('projectCache')
.withAfterEffects({
afterProjectUpdated({ streamId }) {
cache.invalidate(['project', streamId])
},
afterProjectDeleted({ streamId }) {
cache.invalidate(['project', streamId])
},
})
.withGlobalAfterEffect(async ({ type, streamId }) => {
// Invalidate a general "list" cache whenever any event occurs
cache.invalidate(['project-list'])
})
ProjectionProcess ManagerEvent Listener
PurposeBuild queryable read modelsCoordinate workflows across aggregatesLightweight side effects
StatefulPersists to a databaseOptionally stateful (.withState())Stateless
CheckpointedYes — tracks last processed eventYes — can be refreshedNo — fire-and-forget
ReplayableYes (via .withReplay())Yes (via .refreshState())No
Can dispatch commandsNo (throws error)YesYes (via after effects)
Example use caseSQL table mirroring eventsCascade-delete child entitiesCache invalidation, notifications