Beginner14 min readDesign Patternslive prototype

Pub/Sub & Event-driven

Fully decouple who sends from who receives. A *publisher* shouts a message onto a named **topic** ("orders") and has no idea who — if anyone — is listening. A **broker** in the middle keeps the topic lists and hands a copy to every *subscriber* that signed up for it. Nobody has anybody's phone number: add a brand-new listener and the publisher's code never changes by a single line.

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, and publish(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.
typescript
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.

PUBLISHERS Order API Payment API BROKER Event Bus topic: orders topic: payments SUBSCRIBERS EmailService Inventory Analytics orders reaches Email + Inventory · payments reaches Analytics
Publishers emit to topics on the broker; the broker fans each message out only to that topic's subscribers. The two sides share nothing but a topic name.

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 publish naturally 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.

try 01

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.

try 02

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.

try 03

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 nowhere

References & further reading

6 sources

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