Stop wiring callbacks manually, learn how pub–sub makes your code modular, scalable, and surprisingly elegant.

Introduction
Let’s be honest, JavaScript apps are full of “when this happens, do that” logic.
- When a user clicks → update the UI.
- When data loads → show a spinner.
- When payment succeeds → send an email.
It’s easy to start with callbacks or addEventListener()
but as your app grows, everything ends up tangled together.
That’s where the Publish–Subscribe pattern (or Pub–Sub) shines.
It lets you decouple your code so that parts of your app can communicate without knowing about each other.
By the end of this guide, you’ll not only understand how Pub–Sub works, but you’ll know how to build your own version from scratch, use it in real-world scenarios, and recognize it in frameworks you already use.
1️. What Is the Publish–Subscribe Pattern?
At its core, the pattern has three players:
- Publisher → Emits or broadcasts an event.
- Subscriber → Listens for specific events.
- Channel (or Event Bus) → Handles communication between them.
Analogy:
Think of it like a radio station:
- The station (publisher) sends a signal.
- Your radio (subscriber) tunes into that frequency.
- They never directly talk to each other, only through the airwaves (channel).
So when the publisher emits "news"
, all subscribers of "news"
Receive it instantly.
That’s the essence of Pub–Sub.
2️. Why Use Pub–Sub?
As apps grow, direct function calls create tight coupling:
// tightly coupled
function fetchUser() {
// ...
updateUI();
logActivity();
sendNotification();
}
The fetchUser
function now knows too much.
If one function changes, everything else breaks.
Pub–Sub fixes this by turning the flow into events:
eventBus.publish("user:fetched", userData);
Now, anyone can subscribe to "user:fetched"
and react without modifying the core logic.
✅ Better modularity
✅ Easier debugging
✅ Flexible event-driven design
3️. Building a Simple Pub–Sub from Scratch
Let’s implement it step by step.
Step 1: Create an event bus
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach((callback) => callback(data));
}
}
That’s it, a working Pub–Sub system in 10 lines.
Usage:
const bus = new EventBus();
bus.subscribe("user:login", (user) => {
console.log("Welcome,", user.name);
});
bus.subscribe("user:login", () => {
console.log("Log: user logged in");
});
bus.publish("user:login", { name: "Umar" });
Output:
Welcome, Umar
Log: user logged in
✅ One event, multiple reactions
✅ Publisher doesn’t care who’s listening
4️. Adding Unsubscribe Support
In real apps, you’ll want to remove listeners to avoid memory leaks.
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
return () => {
this.events[event] = this.events[event].filter((cb) => cb !== callback);
};
}
publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach((callback) => callback(data));
}
}
Usage:
const unsubscribe = bus.subscribe("data:update", (data) =>
console.log("Data:", data)
);
bus.publish("data:update", { id: 1 }); // works
unsubscribe();
bus.publish("data:update", { id: 2 }); // no output
✅ Now you can clean up listeners dynamically.
5️. Real-World Example Cross-Component Communication
Imagine two completely separate modules:
Header.js
bus.subscribe("theme:change", (theme) => {
document.body.className = theme;
});
Settings.js
const toggleTheme = () => {
const theme = document.body.className === "dark" ? "light" : "dark";
bus.publish("theme:change", theme);
};
document.querySelector("#toggle").addEventListener("click", toggleTheme);
Now, Header.js
updates instantly when it Settings.js
triggers the event
without either file knowing about the other.
✅ True decoupling
✅ Zero import dependencies
6️. Real-World Example Analytics Tracking
bus.subscribe("page:viewed", (data) => {
console.log("Tracking page:", data.url);
});
bus.publish("page:viewed", { url: "/dashboard" });
Even if your tracking logic changes later (Google Analytics → Mixpanel), you don’t touch your core UI logic, just the subscriber.
✅ Separation of concerns
✅ Easier to maintain and replace modules
7️. Async Pub–Sub for Real APIs
Sometimes events depend on asynchronous data.
You can use Promises or async callbacks:
class AsyncBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
async publish(event, data) {
if (!this.events[event]) return;
for (const callback of this.events[event]) {
await callback(data);
}
}
}
Usage:
const bus = new AsyncBus();
bus.subscribe("order:placed", async (order) => {
await sendEmail(order);
console.log("Email sent!");
});
bus.publish("order:placed", { id: 101, total: 499 });
✅ Perfect for network-driven or I/O-heavy events
✅ Keeps logic readable and modular
8️. Pub–Sub in Frameworks You Already Use
Pub–Sub isn’t just a theory, it’s everywhere:

When you call socket.on('message', ...)
In Socket.IO, you’re literally using Pub–Sub under the hood.
9️. Advanced: Namespaced Events
As apps grow, naming collisions can happen.
You can create namespaced events (like "user:login"
or "chat:new"
) or even wildcard matching.
bus.subscribe("user:*", (event) => console.log("User event triggered:", event));
bus.publish("user:logout", { id: 3 });
✅ Adds flexibility for large-scale apps
✅ Mirrors event systems in Redux and WebSocket-based architectures
10. Real Analogy Coffee Shop Broadcast
Think of it like this:
☕ Barista (Publisher): announces “Order ready: Latte!”
👂 Customers (Subscribers): only those waiting for a latte respond.
🎙️ Microphone (Event Bus): transmits the message to everyone in the shop.
If no one’s waiting for a latte, nothing happens.
That’s literally how Pub–Sub works.
Why It Matters
The Pub–Sub pattern turns your codebase from “do this, then that” spaghetti
into event-driven flow, where parts of your app talk through a shared event system.
✅ Loosely coupled architecture
✅ More scalable, maintainable code
✅ Easier debugging and testing
✅ Foundation for real-time systems (WebSockets, Redis, MQTT)
It’s not just a pattern; it’s the backbone of how modern apps communicate.
Conclusion
Pub–Sub makes your code modular and powerful by decoupling communication:
- Publishers emit events without caring who listens.
- Subscribers react without needing to know who triggered it.
- The event bus connects them invisibly.
You can implement it in under 15 lines, yet it scales to millions of messages in real systems.
✅ Understand it once, and you’ll start seeing it everywhere.
The easiest way to understand Pub–Sub is to build it once and then use it everywhere.
Call to Action
Have you used Pub–Sub in your own projects, maybe without realizing it?
Drop a comment 👇 with your favorite example (or a tricky bug you solved with it).
And if your teammate still wires components together manually, share this post and set them free from callback hell.
Leave a Reply