Skip to content

Node.js Event Emitter Patterns — Complete Guide to Advanced Event Architectures

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Node.js Event Emitter Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js event emitter patterns provide reusable architectures for publisher-subscriber, event sourcing, state machines, middleware pipelines, and request-response communication over events.

What You'll Learn

By the end of this tutorial, you'll implement pub-sub systems, event sourcing with replay, state machines using events, middleware chains over EventEmitter, and request-response event patterns.

Why Patterns Matter

Raw EventEmitter is powerful but unstructured. Patterns impose discipline, making event-driven code predictable, debuggable, and maintainable as the system grows in complexity.

Real-World Use

A video processing pipeline uses a state machine pattern: Uploaded, Transcoding, Transcribed, Published. Each state transition emits events that trigger the next step, with error recovery at each stage.

Event Patterns Path

flowchart LR
  A[EventEmitter] --> B[Emitter Patterns]
  B --> C[Streams]
  C --> D[Error Handling]
  D --> E[Worker Threads]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Publisher-Subscriber Pattern

Pub-sub decouples senders from receivers through a central channel. Multiple subscribers can listen to the same event independently.

class PubSub {
  constructor() {
    this.emitter = new EventEmitter();
    this.history = new Map();
  }
  subscribe(event, handler) {
    this.emitter.on(event, handler);
    return () => this.emitter.off(event, handler);
  }
  publish(event, data) {
    if (!this.history.has(event)) this.history.set(event, []);
    this.history.get(event).push({ data, timestamp: Date.now() });
    this.emitter.emit(event, data);
  }
}
const bus = new PubSub();
const unsub = bus.subscribe("user:login", (u) => console.log(`User ${u} logged in`));
bus.publish("user:login", "alice");
unsub();

Event Sourcing Pattern

Event sourcing stores every state change as an event. Current state is derived by replaying all events.

class EventSourcedAccount {
  constructor(id) {
    this.id = id;
    this.balance = 0;
    this.events = [];
    this.emitter = new EventEmitter();
  }
  apply(event) {
    if (event.type === "deposited") this.balance += event.amount;
    if (event.type === "withdrawn") this.balance -= event.amount;
    this.events.push(event);
    this.emitter.emit(event.type, event);
  }
  deposit(amount) {
    this.apply({ type: "deposited", amount, id: this.id });
  }
  withdraw(amount) {
    if (this.balance < amount) throw new Error("Insufficient funds");
    this.apply({ type: "withdrawn", amount, id: this.id });
  }
  replay(events) {
    events.forEach((e) => this.apply(e));
  }
}

Request-Response Over Events

Simulate request-response patterns using events with unique correlation IDs.

class EventRPC {
  constructor() {
    this.emitter = new EventEmitter();
    this.pending = new Map();
  }
  call(method, params) {
    return new Promise((resolve) => {
      const id = `${Date.now()}-${Math.random()}`;
      this.pending.set(id, resolve);
      this.emitter.emit("rpc:request", { id, method, params });
    });
  }
  respond(id, result) {
    const resolve = this.pending.get(id);
    if (resolve) {
      resolve(result);
      this.pending.delete(id);
    }
  }
  onRequest(handler) {
    this.emitter.on("rpc:request", handler);
  }
}

Event-Driven State Machine

Model state transitions as events with guards and side effects.

class StateMachine {
  constructor(initial, transitions) {
    this.state = initial;
    this.transitions = transitions;
    this.emitter = new EventEmitter();
  }
  transition(event) {
    const allowed = this.transitions[this.state]?.[event];
    if (!allowed) throw new Error(`Cannot ${event} from ${this.state}`);
    const prev = this.state;
    this.state = allowed;
    this.emitter.emit("transition", { from: prev, to: this.state, event });
  }
}
const orderSM = new StateMachine("pending", {
  pending: { confirm: "confirmed", cancel: "cancelled" },
  confirmed: { ship: "shipped", cancel: "cancelled" },
  shipped: { deliver: "delivered" },
});
orderSM.emitter.on("transition", (t) => console.log(`${t.from} -> ${t.to} via ${t.event}`));
orderSM.transition("confirm");
orderSM.transition("ship");

Common Mistakes

1. Tightly Coupled Event Handlers

Handlers that directly modify other components break the decoupling benefit. Handlers should emit events, not call methods.

2. No Event Schema Validation

Events without schemas cause runtime errors when producers change payloads. Validate event shapes at publish time.

3. Synchronous Event Chains

Deep synchronous event chains block the event loop. Break chains with setImmediate or Process.nextTick.

4. Missing Error Boundaries

An error in one subscriber should not crash others. Wrap each subscriber in try-catch.

5. Memory Leaks from Unsubscribed Handlers

Long-lived emitters accumulate dead listeners. Always unsubscribe with the returned cleanup function.

Practice Questions

1. What is the difference between pub-sub and Observer Patternver" >}} pattern?

Observer directly notifies observers. Pub-sub uses a channel/broker between publishers and subscribers.

2. How does event sourcing reconstruct state?

By replaying all stored events in order. Each event mutates the state until it matches the current state.

3. Why use correlation IDs in event-driven RPC?

Correlation IDs match request events to response events, enabling asynchronous request-response over events.

4. What is a state machine guard?

A condition that must be true for a transition to be allowed. Guards prevent invalid state changes.

5. Challenge: Implement a middleware pipeline using EventEmitter.

class MiddlewareEmitter extends EventEmitter {
  use(fn) { this.on("middleware", fn); }
  async run(initial) {
    let context = initial;
    const fns = this.listeners("middleware");
    for (const fn of fns) {
      context = await fn(context);
    }
    return context;
  }
}

FAQ

What is the difference between pub-sub and message queue?

Pub-sub broadcasts to all subscribers. Message queues deliver each message to one consumer.

Can I combine EventEmitter with streams?

Yes. Streams extend EventEmitter. You can emit custom events on stream objects for lifecycle hooks.

What is event correlation?

Matching related events across producers and consumers using a shared identifier (correlation ID).

How do you debug event-driven systems?

Log all event emissions with metadata. Use the newListener event to track registrations.

Is EventEmitter suitable for microservices?

Within a process, yes. Across processes, use message brokers like RabbitMQ or Kafka.

Mini Project: Event-Driven Order Processing

Build an order processing pipeline using multiple event patterns.

class OrderProcessor {
  constructor() {
    this.emitter = new EventEmitter();
    this.orders = new Map();
  }
  createOrder(items) {
    const order = { id: Date.now(), items, status: "created", createdAt: new Date() };
    this.orders.set(order.id, order);
    this.emitter.emit("order:created", order);
    return order;
  }
  processPayment(orderId) {
    const order = this.orders.get(orderId);
    if (!order) return this.emitter.emit("error", new Error("Order not found"));
    order.status = "paid";
    order.paidAt = new Date();
    this.emitter.emit("order:paid", order);
    setTimeout(() => this.ship(orderId), 1000);
  }
  ship(orderId) {
    const order = this.orders.get(orderId);
    order.status = "shipped";
    order.shippedAt = new Date();
    this.emitter.emit("order:shipped", order);
  }
}

What's Next

Node.js Streams Node.js Error Handling Node.js Async Patterns

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro