Lesson 48 of 5020 min read

NestJS Event-Driven Architecture Using EventEmitter

Learn how to decouple NestJS services using an event-driven approach with @nestjs/event-emitter, emitting and listening to custom application events.

Author: CodersNexus

NestJS Event-Driven Architecture Using EventEmitter

So far, when one service needed another service to do something, like sending a welcome email after a user registers, you've injected that dependency directly and called its method. This works, but it tightly couples the two services together. Event-driven architecture offers an alternative: instead of directly calling other services, a service emits an event describing something that happened, and any number of independent listeners can react to it, without the emitting service needing to know or care who's listening.

NestJS Event Driven Architecture Eventemitter: Learning Objectives

  • Understand the core idea behind event-driven architecture and the decoupling it provides.
  • Install and configure the official @nestjs/event-emitter package.
  • Emit a custom application event using the injected EventEmitter2 service.
  • Listen for a specific event using the @OnEvent() decorator.
  • Recognize when an event-driven approach is preferable to direct service injection.

NestJS Event Driven Architecture Eventemitter: Key Terms and Definitions

  • Event-driven architecture: A design pattern where components communicate by emitting and listening for events, rather than calling each other's methods directly.
  • @nestjs/event-emitter: The official NestJS package providing EventEmitterModule and an injectable EventEmitter2 service for emitting and listening to events within an application.
  • EventEmitter2: The underlying event emitter library (extending Node.js's built-in EventEmitter) that @nestjs/event-emitter is built on top of.
  • @OnEvent() decorator: Marks a method as a listener that automatically executes whenever a specified event is emitted.
  • Decoupling: Reducing direct dependencies between components, so one component doesn't need to know the specific implementation details of another.

How NestJS Event Driven Architecture Eventemitter Works: Detailed Explanation

Consider the classic example from Module 4: a UsersService that, after successfully creating a new user, needs to trigger a welcome email. The straightforward approach injects a NotificationsService directly into UsersService and calls notificationsService.sendWelcomeEmail(user.email) right there in the create() method. This works, but it means UsersService now directly depends on NotificationsService, and if you later want to also log this event for analytics, or trigger a separate onboarding workflow, you'd need to keep adding more direct dependencies and method calls to UsersService every single time, growing its responsibilities and coupling it more tightly to unrelated concerns.

Event-driven architecture inverts this relationship. Instead of UsersService directly calling NotificationsService, it simply emits an event, something like 'user.created', along with relevant data (the newly created user), using an injected EventEmitter2 instance's emit() method. UsersService's job ends there; it has no idea whether anything is listening for this event, how many listeners exist, or what they do.

Separately, NotificationsService (or any other service, including ones added much later without ever touching UsersService again) can register itself as a listener for this specific event using the @OnEvent('user.created') decorator on one of its methods. When UsersService emits 'user.created', NestJS's event system automatically invokes every registered listener for that event, passing along whatever data was included when the event was emitted. Adding a new listener, say, a separate AnalyticsService also reacting to 'user.created' to log a signup metric, requires zero changes to UsersService itself; you simply add a new @OnEvent('user.created') listener method wherever makes sense.

This pattern is set up using EventEmitterModule.forRoot() imported into your root module, after which any service can inject EventEmitter2 to emit events, and any service can define listener methods decorated with @OnEvent(eventName) to react to them. The core trade-off worth understanding is that this decoupling comes at the cost of directness and traceability: following the flow of what happens after a user is created now requires searching for @OnEvent('user.created') across the codebase rather than simply reading UsersService.create()'s method body top to bottom, a trade-off that becomes increasingly worthwhile as the number of side effects and independent listeners grows.

Interview-Friendly Explanation

A strong interview or viva answer for nestjs event driven architecture eventemitter must go beyond a one-line definition. Interviewers evaluating experienced backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters in production Node.js applications. Use the four-point framework below when answering under time pressure.

  • Definition point: Event-driven architecture: A design pattern where components communicate by emitting and listening for events, rather than calling each other's methods directly.
  • Working point: Install and configure the official @nestjs/event-emitter package.
  • Example point: E-commerce platforms commonly emit an 'order.placed' event, independently triggering inventory deduction, a confirmation email, an analytics log, and a loyalty points update, all as separate listeners rather than one large, tightly-coupled method.
  • Conclusion point: Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.

How to Answer This in a Technical Interview

  1. Give a two-to-three sentence definition using correct NestJS and Node.js terminology.
  2. Add one specific example drawn from a real backend scenario such as an e-commerce, fintech, or SaaS API.
  3. Mention a relevant trade-off or alternative approach, since interviewers often probe this contrast.
  4. Close with one benefit, limitation, or production use case.
  5. Avoid vague answers that only restate the term — interviewers filter these out immediately.

Practical Scenario

Imagine you are scaling a production backend for a SaaS product, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while making that backend more advanced, performant, and maintainable at scale.

NestJS Event Driven Architecture Eventemitter: Architecture and Flow Diagram

Visualize the event-driven flow versus direct coupling:

Direct coupling: [UsersService.create()] --> directly calls --> [NotificationsService.sendWelcomeEmail()]

Event-driven: [UsersService.create()] --> eventEmitter.emit('user.created', user) --> [NestJS Event System] --> notifies all registered listeners independently:
--> [NotificationsService @OnEvent('user.created')]
--> [AnalyticsService @OnEvent('user.created')]
--> [any future listener, added without touching UsersService]

NestJS Event Driven Architecture Eventemitter: Aspect Reference Table

AspectDirect Service InjectionEvent-Driven (EventEmitter2)
CouplingTight — emitting service must know about every consumerLoose — emitting service knows nothing about listeners
Adding new reactionsRequires modifying the original serviceRequires only adding a new listener, no changes to the emitter
TraceabilityEasy to follow — a direct method callRequires searching for @OnEvent() listeners across the codebase
Best forA single, essential, tightly-related operationMultiple independent side effects reacting to the same occurrence

NestJS Event Driven Architecture Eventemitter: NestJS Code Example

// npm install @nestjs/event-emitter

// app.module.ts — enabling the event emitter
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [EventEmitterModule.forRoot()],
})
export class AppModule {}

// users.service.ts — emitting an event instead of calling NotificationsService directly
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

interface CreateUserInput {
  name: string;
  email: string;
}

@Injectable()
export class UsersService {
  constructor(private readonly eventEmitter: EventEmitter2) {}

  create(input: CreateUserInput) {
    const newUser = { id: Date.now(), ...input }; // simulate saving the user

    this.eventEmitter.emit('user.created', newUser); // fire-and-forget; no idea who's listening

    return newUser;
  }
}

// notifications.listener.ts — reacting to the event independently
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class NotificationsListener {
  private readonly logger = new Logger(NotificationsListener.name);

  @OnEvent('user.created')
  handleUserCreatedForNotifications(user: { id: number; email: string }) {
    this.logger.log(`Sending welcome email to ${user.email}`);
  }
}

// analytics.listener.ts — a completely separate listener for the SAME event
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class AnalyticsListener {
  private readonly logger = new Logger(AnalyticsListener.name);

  @OnEvent('user.created')
  handleUserCreatedForAnalytics(user: { id: number }) {
    this.logger.log(`Recording signup metric for user ID ${user.id}`);
  }
}

UsersService.create() no longer injects or calls NotificationsService at all; it simply emits a 'user.created' event with the newly created user's data using eventEmitter.emit(), then returns, entirely unaware of what happens next. NotificationsListener and AnalyticsListener are two completely independent services, each registering their own @OnEvent('user.created') listener method, and both automatically execute whenever that event is emitted, without either one knowing about the other, and critically, without UsersService needing to know about either of them. Adding a third, fourth, or fifth reaction to user creation in the future would only ever require adding a new @OnEvent('user.created') listener somewhere, never touching UsersService itself again.

Real-World NestJS Event Driven Architecture Eventemitter Industry Examples

  • E-commerce platforms commonly emit an 'order.placed' event, independently triggering inventory deduction, a confirmation email, an analytics log, and a loyalty points update, all as separate listeners rather than one large, tightly-coupled method.
  • SaaS platforms use events like 'subscription.cancelled' to trigger independent reactions: sending a retention email, updating internal metrics, and notifying a customer success team, all without the core subscription cancellation logic needing to know about any of them.
  • Audit logging systems frequently listen to a wide range of application events (like 'user.updated' or 'permission.changed') purely to record an audit trail, without being coupled to the specific business logic that triggered each event.
  • Microservice-adjacent architectures sometimes use in-process events like this as a stepping stone before eventually publishing equivalent events to an external message broker (like RabbitMQ or Kafka) for genuinely distributed, cross-service communication.

NestJS Event Driven Architecture Eventemitter Interview Questions and Answers

Q1. What problem does event-driven architecture solve compared to directly injecting and calling other services?

Short answer: Direct service injection tightly couples the calling service to every consumer it directly invokes, meaning adding new reactions to an occurrence requires modifying that original service repeatedly. Event-driven architecture decouples this: a service simply emits an event describing what happened, and any number of independent listeners can react without the emitting service needing to know about them at all.

Detailed explanation: Consider the classic example from Module 4: a UsersService that, after successfully creating a new user, needs to trigger a welcome email. The straightforward approach injects a NotificationsService directly into UsersService and calls notificationsService.sendWelcomeEmail(user.email) right there in the create() method. This works, but it means UsersService now directly depends on NotificationsService, and if you later want to also log this event for analytics, or trigger a separate onboarding workflow, you'd need to keep adding more direct dependencies and method calls to UsersService every single time, growing its responsibilities and coupling it more tightly to unrelated concerns. Event-driven architecture inverts this relationship. Instead of UsersService directly calling NotificationsService, it simply emits an event, something like 'user.created', along with relevant data (the newly created user), using an injected EventEmitter2 instance's emit() method. UsersService's job ends there; it has no idea whether anything is listening for this event, how many listeners exist, or what they do. Separately, NotificationsService (or any other service, including ones added much later without ever touching UsersService again) can register itself as a listener for this specific event using the @OnEvent('user.created') decorator on one of its methods. When UsersService emits 'user.created', NestJS's event system automatically invokes every registered listener for that event, passing along whatever data was included when the event was emitted. Adding a new listener, say, a separate AnalyticsService also reacting to 'user.created' to log a signup metric, requires zero changes to UsersService itself; you simply add a new @OnEvent('user.created') listener method wherever makes sense. This pattern is set up using EventEmitterModule.forRoot() imported into your root module, after which any service can inject EventEmitter2 to emit events, and any service can define listener methods decorated with @OnEvent(eventName) to react to them. The core trade-off worth understanding is that this decoupling comes at the cost of directness and traceability: following the flow of what happens after a user is created now requires searching for @OnEvent('user.created') across the codebase rather than simply reading UsersService.create()'s method body top to bottom, a trade-off that becomes increasingly worthwhile as the number of side effects and independent listeners grows.

Practical example: E-commerce platforms commonly emit an 'order.placed' event, independently triggering inventory deduction, a confirmation email, an analytics log, and a loyalty points update, all as separate listeners rather than one large, tightly-coupled method.

Interview tip: Event-driven architecture decouples an emitting service from any number of independent listeners reacting to what happened.

Revision hook: Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.

Q2. How would you emit and listen for a custom event in NestJS using @nestjs/event-emitter?

Short answer: After importing EventEmitterModule.forRoot(), a service injects EventEmitter2 and calls its emit(eventName, payload) method to fire an event. Any other service can then define a method decorated with @OnEvent(eventName), which NestJS automatically invokes with the emitted payload whenever that specific event occurs.

Detailed explanation: UsersService.create() no longer injects or calls NotificationsService at all; it simply emits a 'user.created' event with the newly created user's data using eventEmitter.emit(), then returns, entirely unaware of what happens next. NotificationsListener and AnalyticsListener are two completely independent services, each registering their own @OnEvent('user.created') listener method, and both automatically execute whenever that event is emitted, without either one knowing about the other, and critically, without UsersService needing to know about either of them. Adding a third, fourth, or fifth reaction to user creation in the future would only ever require adding a new @OnEvent('user.created') listener somewhere, never touching UsersService itself again.

Practical example: SaaS platforms use events like 'subscription.cancelled' to trigger independent reactions: sending a retention email, updating internal metrics, and notifying a customer success team, all without the core subscription cancellation logic needing to know about any of them.

Interview tip: EventEmitterModule.forRoot() must be imported before EventEmitter2 and @OnEvent() function correctly.

Revision hook: Adding a new reaction to an existing event requires zero changes to the original emitting service, a powerful extensibility benefit.

Q3. What is a genuine trade-off of adopting event-driven architecture within a NestJS application?

Short answer: While it improves decoupling, it reduces traceability: following what happens after an event is emitted requires searching the codebase for all @OnEvent() listeners registered for that specific event name, rather than simply reading through a single method's body, which can make the overall flow of logic harder to follow at a glance.

Detailed explanation: Event-driven architecture, implemented in NestJS through the official @nestjs/event-emitter package, decouples services by letting one service emit a named event describing something that happened, using an injected EventEmitter2 instance's emit() method, without needing to know or care what, if anything, reacts to it. Any number of independent services can then register listener methods using the @OnEvent(eventName) decorator, each automatically invoked whenever that event is emitted, allowing new reactions, like notifications, analytics logging, or downstream workflows, to be added over time without ever modifying the original emitting service. This pattern trades some traceability, since following an event's full impact requires searching for its listeners across the codebase, for significant decoupling and extensibility, making it especially valuable whenever a single occurrence, like a user being created or an order being placed, needs to trigger multiple genuinely independent side effects.

Practical example: Audit logging systems frequently listen to a wide range of application events (like 'user.updated' or 'permission.changed') purely to record an audit trail, without being coupled to the specific business logic that triggered each event.

Interview tip: eventEmitter.emit(eventName, payload) fires an event; @OnEvent(eventName) marks a listener method reacting to it.

Revision hook: The reduced traceability trade-off means events work best for genuinely decoupled side effects, not core, essential business logic.

Q4. Give a real-world example where an event-driven approach would be preferable to direct service calls.

Short answer: An 'order.placed' event in an e-commerce platform is a strong example, since a single order placement typically needs to trigger multiple independent reactions, inventory deduction, a confirmation email, analytics logging, and a loyalty points update, each of which can be implemented as a separate, decoupled listener rather than one method directly calling four different services.

Detailed explanation: Consider the classic example from Module 4: a UsersService that, after successfully creating a new user, needs to trigger a welcome email. The straightforward approach injects a NotificationsService directly into UsersService and calls notificationsService.sendWelcomeEmail(user.email) right there in the create() method. This works, but it means UsersService now directly depends on NotificationsService, and if you later want to also log this event for analytics, or trigger a separate onboarding workflow, you'd need to keep adding more direct dependencies and method calls to UsersService every single time, growing its responsibilities and coupling it more tightly to unrelated concerns. Event-driven architecture inverts this relationship. Instead of UsersService directly calling NotificationsService, it simply emits an event, something like 'user.created', along with relevant data (the newly created user), using an injected EventEmitter2 instance's emit() method. UsersService's job ends there; it has no idea whether anything is listening for this event, how many listeners exist, or what they do. Separately, NotificationsService (or any other service, including ones added much later without ever touching UsersService again) can register itself as a listener for this specific event using the @OnEvent('user.created') decorator on one of its methods. When UsersService emits 'user.created', NestJS's event system automatically invokes every registered listener for that event, passing along whatever data was included when the event was emitted. Adding a new listener, say, a separate AnalyticsService also reacting to 'user.created' to log a signup metric, requires zero changes to UsersService itself; you simply add a new @OnEvent('user.created') listener method wherever makes sense. This pattern is set up using EventEmitterModule.forRoot() imported into your root module, after which any service can inject EventEmitter2 to emit events, and any service can define listener methods decorated with @OnEvent(eventName) to react to them. The core trade-off worth understanding is that this decoupling comes at the cost of directness and traceability: following the flow of what happens after a user is created now requires searching for @OnEvent('user.created') across the codebase rather than simply reading UsersService.create()'s method body top to bottom, a trade-off that becomes increasingly worthwhile as the number of side effects and independent listeners grows.

Practical example: Microservice-adjacent architectures sometimes use in-process events like this as a stepping stone before eventually publishing equivalent events to an external message broker (like RabbitMQ or Kafka) for genuinely distributed, cross-service communication.

Interview tip: Trade-off: better decoupling and extensibility, at the cost of reduced traceability compared to direct method calls.

Revision hook: Consistent, clear event naming (like 'user.created', 'order.placed') is what keeps an event-driven codebase navigable as it grows.

NestJS Event Driven Architecture Eventemitter MCQs and Practice Questions

1. Which official NestJS package provides EventEmitter2 and the @OnEvent() decorator?

  1. @nestjs/schedule
  2. @nestjs/event-emitter
  3. @nestjs/cache-manager
  4. @nestjs/throttler

Answer: B. @nestjs/event-emitter

Explanation: @nestjs/event-emitter is the official package providing EventEmitterModule, the injectable EventEmitter2 service, and the @OnEvent() decorator for building event-driven functionality in NestJS.

Concept link: Event-driven architecture decouples an emitting service from any number of independent listeners reacting to what happened.

Why this matters: Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.

2. What method is used to emit a custom event using an injected EventEmitter2 instance?

  1. eventEmitter.trigger()
  2. eventEmitter.emit()
  3. eventEmitter.dispatch()
  4. eventEmitter.fire()

Answer: B. eventEmitter.emit()

Explanation: The emit() method, called on an injected EventEmitter2 instance, is used to fire a named event along with an optional payload, which any registered listeners for that event name will then receive.

Concept link: EventEmitterModule.forRoot() must be imported before EventEmitter2 and @OnEvent() function correctly.

Why this matters: Adding a new reaction to an existing event requires zero changes to the original emitting service, a powerful extensibility benefit.

3. Which decorator marks a method as a listener for a specific event?

  1. @Cron()
  2. @Interval()
  3. @OnEvent(eventName)
  4. @Emit(eventName)

Answer: C. @OnEvent(eventName)

Explanation: @OnEvent(eventName) marks a method to automatically execute whenever an event matching that name is emitted anywhere in the application, receiving the event's payload as its argument.

Concept link: eventEmitter.emit(eventName, payload) fires an event; @OnEvent(eventName) marks a listener method reacting to it.

Why this matters: The reduced traceability trade-off means events work best for genuinely decoupled side effects, not core, essential business logic.

4. What is the main trade-off of adopting an event-driven approach over direct service calls?

  1. It always makes the application slower
  2. It reduces coupling but makes tracing the full flow of logic across the codebase harder
  3. It removes the need for dependency injection
  4. It eliminates the possibility of bugs

Answer: B. It reduces coupling but makes tracing the full flow of logic across the codebase harder

Explanation: While event-driven architecture meaningfully reduces coupling between services, it comes at the cost of traceability, since following what happens after an event is emitted requires searching for all its listeners rather than reading a single, direct method call chain.

Concept link: Trade-off: better decoupling and extensibility, at the cost of reduced traceability compared to direct method calls.

Why this matters: Consistent, clear event naming (like 'user.created', 'order.placed') is what keeps an event-driven codebase navigable as it grows.

Common NestJS Event Driven Architecture Eventemitter Mistakes to Avoid

  • Overusing events for simple, single, essential operations that would be clearer and more traceable as a direct method call.
  • Forgetting to import EventEmitterModule.forRoot(), causing @OnEvent() listeners to never actually be registered or triggered.
  • Using inconsistent or unclear event naming conventions across the application, making it hard to know which events exist or what data they carry.
  • Assuming event listeners execute in a guaranteed order relative to each other, when in most default configurations they don't provide strict ordering guarantees.

NestJS Event Driven Architecture Eventemitter: Interview Notes and Exam Tips

  • Event-driven architecture decouples an emitting service from any number of independent listeners reacting to what happened.
  • EventEmitterModule.forRoot() must be imported before EventEmitter2 and @OnEvent() function correctly.
  • eventEmitter.emit(eventName, payload) fires an event; @OnEvent(eventName) marks a listener method reacting to it.
  • Trade-off: better decoupling and extensibility, at the cost of reduced traceability compared to direct method calls.

Key NestJS Event Driven Architecture Eventemitter Takeaways

  • Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.
  • Adding a new reaction to an existing event requires zero changes to the original emitting service, a powerful extensibility benefit.
  • The reduced traceability trade-off means events work best for genuinely decoupled side effects, not core, essential business logic.
  • Consistent, clear event naming (like 'user.created', 'order.placed') is what keeps an event-driven codebase navigable as it grows.

NestJS Event Driven Architecture Eventemitter: Summary

Event-driven architecture, implemented in NestJS through the official @nestjs/event-emitter package, decouples services by letting one service emit a named event describing something that happened, using an injected EventEmitter2 instance's emit() method, without needing to know or care what, if anything, reacts to it. Any number of independent services can then register listener methods using the @OnEvent(eventName) decorator, each automatically invoked whenever that event is emitted, allowing new reactions, like notifications, analytics logging, or downstream workflows, to be added over time without ever modifying the original emitting service. This pattern trades some traceability, since following an event's full impact requires searching for its listeners across the codebase, for significant decoupling and extensibility, making it especially valuable whenever a single occurrence, like a user being created or an order being placed, needs to trigger multiple genuinely independent side effects.

Frequently Asked Questions

No, events are best reserved for genuinely independent side effects reacting to something that happened, not for essential, core business logic where a direct method call is clearer, more traceable, and doesn't need the flexibility of multiple independent listeners. In interviews, tie this back to: Event-driven architecture decouples an emitting service from any number of independent listeners reacting to what happened. In real applications, consider this example: E-commerce platforms commonly emit an 'order.placed' event, independently triggering inventory deduction, a confirmation email, an analytics log, and a loyalty points update, all as separate listeners rather than one large, tightly-coupled method. Key revision takeaway: Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.

Yes, multiple listeners can register for the same event name, and by default, EventEmitter2 does not provide strict, guaranteed ordering between multiple listeners for the same event, so listeners should generally be designed to work independently of any particular execution order. In interviews, tie this back to: EventEmitterModule.forRoot() must be imported before EventEmitter2 and @OnEvent() function correctly. In real applications, consider this example: SaaS platforms use events like 'subscription.cancelled' to trigger independent reactions: sending a retention email, updating internal metrics, and notifying a customer success team, all without the core subscription cancellation logic needing to know about any of them. Key revision takeaway: Adding a new reaction to an existing event requires zero changes to the original emitting service, a powerful extensibility benefit.

No, @nestjs/event-emitter handles events entirely in-process, within a single running application instance, whereas a message queue like RabbitMQ or Kafka enables genuinely distributed, cross-service or cross-instance communication; some teams use in-process events as a stepping stone before adopting a full message queue for distributed scenarios. In interviews, tie this back to: eventEmitter.emit(eventName, payload) fires an event; @OnEvent(eventName) marks a listener method reacting to it. In real applications, consider this example: Audit logging systems frequently listen to a wide range of application events (like 'user.updated' or 'permission.changed') purely to record an audit trail, without being coupled to the specific business logic that triggered each event. Key revision takeaway: The reduced traceability trade-off means events work best for genuinely decoupled side effects, not core, essential business logic.

Nothing happens beyond the emit() call itself completing; EventEmitter2 doesn't throw an error or warning by default if an event has no registered listeners, so it's worth being mindful that a typo in an event name could silently result in no listener ever being triggered. In interviews, tie this back to: Trade-off: better decoupling and extensibility, at the cost of reduced traceability compared to direct method calls. In real applications, consider this example: Microservice-adjacent architectures sometimes use in-process events like this as a stepping stone before eventually publishing equivalent events to an external message broker (like RabbitMQ or Kafka) for genuinely distributed, cross-service communication. Key revision takeaway: Consistent, clear event naming (like 'user.created', 'order.placed') is what keeps an event-driven codebase navigable as it grows.

Yes, @OnEvent() listener methods can be async functions performing any asynchronous work, including database queries or external API calls, exactly like any other NestJS service method, though by default the emitting service's emit() call doesn't wait for listeners to complete unless specifically configured to do so. In interviews, tie this back to: Event-driven architecture decouples an emitting service from any number of independent listeners reacting to what happened. In real applications, consider this example: E-commerce platforms commonly emit an 'order.placed' event, independently triggering inventory deduction, a confirmation email, an analytics log, and a loyalty points update, all as separate listeners rather than one large, tightly-coupled method. Key revision takeaway: Event-driven architecture is most valuable when a single occurrence needs to trigger multiple, genuinely independent reactions.

A common, clear convention uses a 'subject.action' or 'entity.event' pattern, such as 'user.created', 'order.placed', or 'subscription.cancelled', making it immediately clear what occurred and keeping event names consistent and searchable across a growing codebase. In interviews, tie this back to: EventEmitterModule.forRoot() must be imported before EventEmitter2 and @OnEvent() function correctly. In real applications, consider this example: SaaS platforms use events like 'subscription.cancelled' to trigger independent reactions: sending a retention email, updating internal metrics, and notifying a customer success team, all without the core subscription cancellation logic needing to know about any of them. Key revision takeaway: Adding a new reaction to an existing event requires zero changes to the original emitting service, a powerful extensibility benefit.