The idea
What it is
Pub/Sub (publish–subscribe) takes decoupling to its limit. Its intent in one sentence: route messages through a broker keyed by topic, so producers and consumers never know each other exists. A publisher emits a message to a named topic — it doesn't call anyone, doesn't hold a list, doesn't know if a single soul is listening. A subscriber registers with the broker for the topics it cares about. The broker in the middle keeps the topic-to-subscriber lists and delivers a copy of each message to everyone who signed up. The two sides are mutually anonymous.
The mental model is a radio station plus a topic-based mailing list. The station broadcasts on a frequency ("orders"); it has no idea how many radios are tuned in, or whose. Anyone who tuned to that frequency hears it; anyone who didn't, hears nothing. The station never phones a listener, and a listener never phones the station — the airwaves (the broker) sit between them. Want to start listening? Tune in. Want to stop? Tune out. The broadcaster's routine doesn't change either way.
Event-driven architecture is this idea scaled up to whole systems: instead of services calling each other directly, each one reacts to a stream of events flowing through a broker. "Order placed" is published once; the email service, the inventory service, and the analytics service each react on their own. Add a fraud-check service next year and you subscribe it to the same event — nobody who already publishes "order placed" has to be touched.
The one sentence to remember
Observer's subject holds the list and calls its observers itself; pub/sub inserts a broker so publishers emit to a topic and subscribers register with the broker — the two sides never reference each other, and delivery can be async or across a network.
You've already used it today
The browser's EventTarget / DOM CustomEvent bus, Node's EventEmitter, Redis PUBLISH/SUBSCRIBE, Kafka and RabbitMQ topics, and cloud services like AWS SNS/SQS or Google Cloud Pub/Sub are all this pattern — publishers dropping messages onto named channels for anonymous subscribers to pick up.
Mechanics
How it works
The three roles
Where Observer has two participants, pub/sub has three — and the new one, the broker, is the whole point:
- Publisher — produces a message and hands it to the broker on a named topic:
publish("orders", msg). That's it. It never holds a subscriber list, never loops over anyone, and gets no answer back about who received it. - Broker (the event bus) — the middleman. It owns the topic-to-subscriber lists and exposes two doors:
subscribe(topic, handler)adds a handler to a topic's list, andpublish(topic, msg)looks up that topic's list and delivers a copy to every handler on it. Publishers and subscribers only ever talk to this. - Subscriber — registers a handler with the broker for the topics it cares about, then reacts whenever a message arrives. It never knows who published, or whether other subscribers exist.
type Handler = (payload: unknown) => void;
class EventBus { // the BROKER
private topics: Record<string, Handler[]> = {}; // topic -> subscriber list
subscribe(topic: string, handler: Handler) { // a subscriber signs up
(this.topics[topic] ||= []).push(handler);
}
publish(topic: string, payload: unknown) { // a publisher emits
for (const h of this.topics[topic] || []) { // route to THIS topic only
h(payload); // deliver a copy to each
}
}
}Read the two methods carefully: publish never mentions a subscriber, and subscribe never mentions a publisher. They only share a string — the topic name. That string is the entire contract between the two sides.
Topic routing — the message only goes where it's addressed
A message published to "orders" reaches only the handlers that subscribed to "orders". A subscriber tuned to "payments" hears nothing. This is what makes one broker able to carry many independent conversations at once without them bleeding into each other — the topic is the channel.
The decoupling payoff
Because the two sides only share a topic name, you can add, remove, or replace subscribers freely — while the system runs, and even in other processes or machines — without the publisher noticing. This is the headline benefit:
- Add a consumer with zero producer changes — a new analytics service subscribes to
"orders"; the order publisher's code is untouched. - Cross a boundary — the broker can be an in-memory object, or a network service (Redis, Kafka, SNS). Publisher and subscriber can live in different processes, languages, or data centres.
- Fan out for free — one
publishnaturally reaches many subscribers; going from one consumer to five is a subscription change, not a rewrite.
Observer vs pub/sub — close cousins, not twins
In [[observer]], the subject holds its observer list itself and calls each observer's update() synchronously, in the same process — the two sides are coupled at least through that interface, and the observer knows which subject it joined. Pub/sub adds a broker in the middle: publishers emit to a topic, subscribers register with the broker, and neither side references the other at all — not even by interface. That extra hop is what lets delivery become asynchronous or travel over a network. If your middleman starts making decisions about who should talk to whom (not just fanning out by topic), you're drifting toward a [[mediator]].
Same word, two scopes
"Pub/sub" describes both a tiny in-process event bus (Node EventEmitter, a DOM CustomEvent bus) and a heavyweight message broker across a network (Kafka, RabbitMQ, SNS/SQS). Same shape — publisher, topic, broker, subscriber — very different delivery guarantees.
The trade-offs of going event-driven
The anonymity that buys you decoupling also takes something away: you can no longer read the flow straight off the code.
- "Who handled this?" — a publisher just drops a message and moves on. Following what actually happens next means knowing every subscriber, which isn't visible at the publish site. Debugging shifts from reading a call stack to tracing events.
- Eventual consistency — subscribers react on their own schedule, so the system is briefly out of sync after each event. You give up the instant, all-in-one-transaction feel of a direct call.
- Delivery guarantees — a networked broker chooses between at-least-once (may deliver duplicates — handlers must be idempotent) and at-most-once (may drop). Exactly-once is famously hard.
- Ordering — messages can arrive out of order, especially across partitions or retries; if order matters you must design for it explicitly.
- The invisible web — it's easy to grow an untraceable tangle where event A fires B fires C, and no one can say what a single publish will ultimately trigger.
Fire-and-forget means no answer
Publishing is one-way: you do not get a return value telling you who received the message or what they did with it. If you need a synchronous result back, pub/sub is the wrong tool — make a direct call instead.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A 📮 Broker / Event Bus in the middle holding two topics — 🟢 orders and 🔵 payments — with three services on the right that each choose which topics to subscribe to. Flip the orders chip on EmailService and InventoryService, then hit ▶ Publish to orders: a message dot flies from the publisher into the broker and fans out to only the orders subscribers — their 📥 inbox ticks, everyone else stays dim. Notice the publisher's dot always stops at the broker: it has no wire to the services and never learns who received it. Add or drop a subscriber with a chip and publish again — a different set gets the message, and the banner still reads publisher code changed: 0 lines.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Subscribe some services to a topic
On the right, click the 🟢 orders chip on EmailService and InventoryService — each chip lights up as one subscribe("orders", handler) call landing in the broker's orders list. The broker's orders topic now shows 2 subscribers. Nothing has been delivered yet: subscribing only registers interest.
Publish and watch topic routing
Hit ▶ Publish to orders. A message dot flies from the publisher into the broker's orders topic, then fans out to only EmailService and InventoryService — their 📥 inbox badges tick +1 while AnalyticsService stays dim, skipped. Crucially the publisher's dot stops at the broker: it never touches a service and never learns who received it. Now hit ▶ Publish to payments — with no payments subscribers, the message reaches the broker and goes nowhere.
Add a subscriber at runtime — zero publisher changes
Flip the 🔵 payments chip on AnalyticsService, then publish to payments again. This time the message routes to Analytics, and the banner reads 'publisher code changed: 0 lines' — you changed who listens without touching the publisher. Toggle chips on and off and re-publish to see a different set light up each time. Hit ↺ Reset to start clean.
In practice
When to use it — and what trips people up
Reach for pub/sub when...
- Many consumers must react to the same event, and the set keeps changing — one "order placed" needs to reach email, inventory, and analytics today, and fraud-check tomorrow, without editing the producer.
- Producers and consumers should be independently deployable — decoupling through a broker lets teams ship, scale, and restart their services on their own schedule.
- Work can happen asynchronously — the publisher doesn't need to wait for or hear back from the consumers; fire-and-forget is fine, or even desirable for smoothing load spikes.
- You're building an event-driven system across process or network boundaries — where a shared broker (Kafka, RabbitMQ, SNS/SQS) is the natural backbone.
Skip it when...
- A simple direct call is clearer — if A always calls B and only B, a broker and a topic name are indirection with no payoff. Just call the function.
- You need a guaranteed synchronous result back — pub/sub is fire-and-forget; it returns nothing about who received the message. Use a direct call or request/response instead.
- Strict ordering, tracing, or transactions matter more than decoupling — when you must reason precisely about the sequence of effects or roll them back atomically, the event web's opacity works against you.
What it gives you
- Maximum decoupling — publishers and subscribers are mutually anonymous, sharing nothing but a topic name, so either side can change or be redeployed independently.
- Add consumers with zero producer changes — a new subscriber on an existing topic requires not a single line changed in any publisher.
- Natural fan-out and scale — one publish reaches many subscribers, and the broker can absorb bursts and buffer work across processes and machines.
- Crosses boundaries — the same shape works in-process or over a network, letting components live in different processes, languages, or data centres.
Common mistakes
- Hard to trace flow — 'who handled this?' isn't answerable from the publish site; debugging means following events, not a call stack.
- Eventual consistency — subscribers react on their own schedule, so the system is briefly inconsistent after each event and you lose transactional all-or-nothing semantics.
- Delivery and ordering caveats — networked brokers force a choice between at-least-once (duplicates) and at-most-once (drops), and messages can arrive out of order.
- The invisible web — unchecked, events can chain into an untraceable tangle where no one can predict what a single publish ultimately triggers.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
type Handler = (payload: unknown) => void;
// The BROKER — the only thing publishers and subscribers ever touch.
class EventBus {
private topics: Record<string, Handler[]> = {}; // topic -> subscriber list
subscribe(topic: string, handler: Handler): void {
(this.topics[topic] ||= []).push(handler); // sign up for a topic
}
publish(topic: string, payload: unknown): void {
for (const h of this.topics[topic] || []) { // route to THIS topic only
h(payload); // deliver a copy to each
}
}
}
const bus = new EventBus();
// Two subscribers on ONE topic — they don't know each other or the publisher.
bus.subscribe("orders", (o) => console.log("📧 email:", o));
bus.subscribe("orders", (o) => console.log("📦 stock:", o));
// A publisher that knows nobody — just a topic name and a payload.
bus.publish("orders", { id: 42 }); // → both handlers run
bus.publish("payments", { id: 42 }); // → nobody subscribed, goes nowhereReferences & further reading
6 sources- Articlemartinfowler.com
What do you mean by "Event-Driven"? — Martin Fowler
Untangles the four different things people mean by 'event-driven' (event notification, event-carried state transfer, event sourcing, CQRS). Essential for not conflating them.
- Articleenterpriseintegrationpatterns.com
Publish-Subscribe Channel — Enterprise Integration Patterns
The canonical pattern definition: one input channel, a copy delivered to each subscriber. The reference vocabulary for messaging systems.
- Docscloud.google.com
What is Pub/Sub? — Google Cloud
A clear vendor overview of topics, subscriptions, publishers, and subscribers, plus at-least-once delivery and the push/pull subscription models.
- Docsdocs.aws.amazon.com
Amazon SNS — How it works (AWS docs)
SNS is pub/sub as a managed service: publishers send to a topic, and each subscription (queue, function, endpoint) gets a copy. A concrete cloud implementation.
- Docskafka.apache.org
Apache Kafka — Introduction
The dominant event-streaming broker. Explains topics, partitions, producers, and consumers, and how ordering and retention actually work at scale.
- Docsdeveloper.mozilla.org
CustomEvent — MDN
Pub/sub in the browser: dispatch a named CustomEvent and any addEventListener handler for that name receives it — an in-page event bus you already have.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 06
In pub/sub, what sits between publishers and subscribers?
question 02 / 06
How does pub/sub differ from the classic Observer pattern?
question 03 / 06
What does a publisher know about its subscribers?
question 04 / 06
A publisher emits a message on the topic "orders". Who receives it?
question 05 / 06
You want to add a new AnalyticsService that reacts to every order. What has to change in the order publisher?
question 06 / 06
Which is a genuine downside of going event-driven with pub/sub?
0/6 answered