JavaScript Interview Prep
Design Patterns

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


Observer and PubSub thumbnail


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 addEventListener is 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

AspectObserverPubSub
CouplingSubject knows observers existPublishers and subscribers are fully decoupled
CommunicationDirect (subject -> observer)Via message broker/channel
Event knowledgeObserver knows the subjectOnly knows the channel name
Use caseComponent-level state trackingApplication-wide messaging
ExampleReact state, MutationObserverEvent 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)

Observer and PubSub visual 1


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 notify loop — wrap each invocation in try/catch so 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. addEventListener is 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?

  1. 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 Observer above.) Store callbacks keyed by event, push in subscribe, filter in unsubscribe, iterate and invoke in notify. Return an unsubscribe function from subscribe for 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.

On this page