Observer and PubSub
Subscribe, Don't Poll
LinkedIn Hook
You do not refresh YouTube every 30 seconds to check whether your favourite creator uploaded a video. You subscribe. They push.
That single shift — from "pull" to "push" — is the Observer pattern, and it is the quiet engine behind almost every reactive system you have ever touched:
addEventListener, Redux, MobX, React state, MutationObserver, Node streams.The variant you see in large apps — PubSub — goes one step further. It adds a message broker between publisher and subscriber so neither side has to know the other exists. That is what makes plugin systems, cross-module events, and decoupled microfrontends possible.
In this lesson you will implement both patterns from scratch, learn exactly where each one fits, and avoid the memory-leak trap that takes down every junior PubSub implementation.
Read the full lesson -> [link]
#JavaScript #ObserverPattern #PubSub #DesignPatterns #EventDriven #InterviewPrep #Frontend
What You'll Learn
- How the Observer pattern implements push-based notification with a single subject
- How PubSub decouples publishers and subscribers through a message broker
- Where each pattern fits, why
addEventListeneris Observer in disguise, and how to avoid leaks
The YouTube Subscription Model
Think of a YouTube channel. When you subscribe, you get notified whenever a new video is uploaded. You don't check the channel every 5 minutes — the channel pushes notifications to all subscribers. That's the Observer pattern: one subject, many observers, automatic notification on state change.
Observer Pattern — Full Implementation
class Observer {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (typeof callback !== "function") {
throw new TypeError("Callback must be a function");
}
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
// Prevent duplicate subscriptions
if (this.subscribers[event].includes(callback)) {
return () => this.unsubscribe(event, callback);
}
this.subscribers[event].push(callback);
// Return unsubscribe function for convenience
return () => this.unsubscribe(event, callback);
}
unsubscribe(event, callback) {
if (!this.subscribers[event]) return;
this.subscribers[event] = this.subscribers[event].filter(
(cb) => cb !== callback
);
// Clean up empty event arrays
if (this.subscribers[event].length === 0) {
delete this.subscribers[event];
}
}
notify(event, ...data) {
if (!this.subscribers[event]) return;
this.subscribers[event].forEach((callback) => {
try {
callback(...data);
} catch (error) {
console.error(`Error in subscriber for "${event}":`, error);
}
});
}
// Check how many subscribers exist for an event
listenerCount(event) {
return this.subscribers[event]?.length || 0;
}
// Clear all subscribers for an event (or all events)
clear(event) {
if (event) {
delete this.subscribers[event];
} else {
this.subscribers = {};
}
}
}
// Usage
const store = new Observer();
// Subscribe
const unsubName = store.subscribe("userUpdated", (user) => {
console.log(`Name display updated: ${user.name}`);
});
store.subscribe("userUpdated", (user) => {
console.log(`Avatar updated: ${user.avatar}`);
});
store.subscribe("orderPlaced", (order) => {
console.log(`Order #${order.id} placed!`);
});
// Notify all subscribers of "userUpdated"
store.notify("userUpdated", { name: "Alice", avatar: "alice.png" });
// "Name display updated: Alice"
// "Avatar updated: alice.png"
// Unsubscribe using returned function
unsubName();
store.notify("userUpdated", { name: "Bob", avatar: "bob.png" });
// "Avatar updated: bob.png" (name handler was removed)
store.listenerCount("userUpdated"); // 1
PubSub Variation — Event Channel
The PubSub pattern adds a central "message broker" so publishers and subscribers don't know about each other at all:
class PubSub {
constructor() {
this.channels = {};
this.idCounter = 0;
}
publish(channel, data) {
if (!this.channels[channel]) return [];
const results = [];
this.channels[channel].forEach((sub) => {
try {
results.push(sub.callback(data));
} catch (err) {
console.error(`PubSub error on channel "${channel}":`, err);
}
});
return results;
}
subscribe(channel, callback) {
if (!this.channels[channel]) {
this.channels[channel] = [];
}
const id = ++this.idCounter;
this.channels[channel].push({ id, callback });
// Return a token for unsubscribing
return {
unsubscribe: () => {
this.channels[channel] = this.channels[channel].filter(
(sub) => sub.id !== id
);
}
};
}
// One-time subscription
subscribeOnce(channel, callback) {
const sub = this.subscribe(channel, (data) => {
callback(data);
sub.unsubscribe();
});
return sub;
}
}
// Usage — publishers and subscribers are completely decoupled
const bus = new PubSub();
// Subscriber A (analytics module)
bus.subscribe("purchase", (data) => {
console.log(`Analytics: tracked purchase of $${data.amount}`);
});
// Subscriber B (email module)
bus.subscribe("purchase", (data) => {
console.log(`Email: sending receipt to ${data.email}`);
});
// Publisher (checkout module) — has NO idea who is listening
bus.publish("purchase", {
amount: 99.99,
email: "user@example.com",
itemId: "SKU-123"
});
// "Analytics: tracked purchase of $99.99"
// "Email: sending receipt to user@example.com"
Observer vs PubSub — Key Differences
| Aspect | Observer | PubSub |
|---|---|---|
| Coupling | Subject knows observers exist | Publishers and subscribers are fully decoupled |
| Communication | Direct (subject -> observer) | Via message broker/channel |
| Event knowledge | Observer knows the subject | Only knows the channel name |
| Use case | Component-level state tracking | Application-wide messaging |
| Example | React state, MutationObserver | Event bus, Redux middleware |
Relationship to addEventListener
// addEventListener IS the Observer pattern!
// The DOM element is the Subject
// Your callback is the Observer
const button = document.querySelector("#submit");
// Subscribe (observe)
function handleClick(event) {
console.log("Button clicked!");
}
button.addEventListener("click", handleClick);
// Unsubscribe (stop observing)
button.removeEventListener("click", handleClick);
// The button notifies ALL registered listeners when clicked
// This is exactly: subject.notify("click", event)
When to Use / When NOT to Use
Use Observer/PubSub for:
- Decoupled communication between modules
- State management (Redux, MobX use Observer internally)
- Real-time UI updates when data changes
- Plugin/extension systems
Avoid when:
- Simple direct communication between two known modules (just call the function)
- The system becomes a "spaghetti of events" — too many pub/sub channels make debugging hard
- Performance-critical hot paths (the indirection has overhead)
Common Mistakes
- Forgetting to unsubscribe — listeners keep references to closed-over state and silently leak memory, especially in SPA route transitions.
- Registering duplicate callbacks and firing them twice — either dedupe in
subscribe(as shown) or document that it is the caller's responsibility. - Letting one listener throw and kill the whole
notifyloop — wrap each invocation intry/catchso one bad subscriber does not silence the rest.
Interview Questions
Q: What is the Observer pattern? How does it relate to addEventListener?
The Observer pattern defines a one-to-many relationship where a subject maintains a list of observers and notifies them of state changes.
addEventListeneris the browser's built-in Observer implementation: the DOM element is the subject, your callback is the observer, and events like "click" trigger notifications.
Q: What is the difference between Observer and PubSub?
In Observer, the subject directly references and notifies observers — there's a direct relationship. In PubSub, a message broker sits between publishers and subscribers. Publishers emit events to the broker, subscribers listen on the broker. Neither knows the other exists, providing complete decoupling.
Q: What are the downsides of the PubSub pattern?
- Debugging is hard — you can't easily trace who published an event or who is subscribing. 2) Memory leaks if you forget to unsubscribe. 3) Event naming collisions. 4) No type safety on event payloads. 5) Difficult to track data flow in complex systems.
Q: Implement a basic Observer with subscribe, unsubscribe, and notify.
(See
class Observerabove.) Store callbacks keyed by event, push insubscribe, filter inunsubscribe, iterate and invoke innotify. Return an unsubscribe function fromsubscribefor ergonomic cleanup.
Quick Reference — Cheat Sheet
OBSERVER / PUBSUB — QUICK MAP
Observer:
subject -----> notifies directly ----> observers
API: subscribe(event, cb) / unsubscribe / notify
PubSub:
publisher --> event bus --> subscribers (no direct link)
API: publish(channel, data) / subscribe / subscribeOnce
Who knows whom?
Observer : subject references observers
PubSub : neither side knows the other; broker in the middle
Browser built-in:
addEventListener = Observer (DOM element is the subject)
Traps:
- forgetting to unsubscribe -> memory leak
- one listener throws -> try/catch around each call
- too many channels -> "spaghetti of events"
Previous: Singleton Pattern -> The One-Instance Rule Next: Factory Pattern -> Build by Description, Not Assembly
This is Lesson 12.3 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.