Node.js Event Emitter Patterns — Complete Guide to Advanced Event Architectures
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
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