Event Bus and Observer Pattern in JavaScript (Node.js)
In modern Node.js development, it is often necessary to organize interaction between different application components without creating tight couplings between them. The Observer pattern and its more powerful implementation — the Event Bus — provide an elegant solution to this problem. In this article, we will take a detailed look at how to implement these patterns in pure JavaScript within the Node.js environment, discuss their advantages, and typical use cases.
What is the Observer Pattern?
The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes its state, all dependent objects (observers) are automatically notified. In the context of Node.js, this pattern underlies the built-in events module, which provides the EventEmitter class.
Event Bus: Extending the Concept
An Event Bus is a centralized event channel that allows any application component to send and receive messages without knowing about each other directly. This is especially useful in microservice architectures, complex applications with a modular structure, or when creating plugin systems.
Implementing an Event Bus in Node.js
Let's look at creating our own Event Bus from scratch. Our class will support the main methods: subscribing to events (on), one-time subscription (once), removing subscribers (off), and emitting events (emit).
class EventBus { constructor() { this.events = {}; }
on(event, listener) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(listener); return () => this.off(event, listener); }
once(event, listener) { const wrapper = (...args) => { listener(...args); this.off(event, wrapper); }; this.on(event, wrapper); return () => this.off(event, wrapper); }
off(event, listener) { if (!this.events[event]) return; this.events[event] = this.events[event].filter(l => l !== listener); }
emit(event, ...args) { if (!this.events[event]) return false; this.events[event].forEach(listener => listener(...args)); return true; }}
// Usage exampleconst bus = new EventBus();
const unsubscribe = bus.on('user:login', (username) => { console.log(`User ${username} logged in`);});
bus.once('app:start', () => { console.log('Application started (one-time notification)');});
// Emitting eventsbus.emit('app:start'); // "Application started (one-time notification)"bus.emit('user:login', 'Anna'); // "User Anna logged in"
// Unsubscribingunsubscribe();bus.emit('user:login', 'Ivan'); // Nothing happensUsing the Built-in EventEmitter from Node.js
Node.js provides a ready-made EventEmitter implementation in the events module. However, an Event Bus built on top of it allows for centralized management of all application events.
const EventEmitter = require('events');
class AppEventBus extends EventEmitter { constructor() { super(); this.setMaxListeners(50); // Increase the subscriber limit }
// Add logging for debugging emit(event, ...args) { console.log(`[EventBus] Event: ${event}`); return super.emit(event, ...args); }}
const appBus = new AppEventBus();
// Subscribing to eventsappBus.on('order:created', (order) => { console.log(`Order #${order.id} created. Sending notification.`);});
appBus.on('order:created', (order) => { console.log(`Order #${order.id} created. Updating inventory.`);});
// EmissionappBus.emit('o