The idea: two ways to announce an event

Something happens in one part of a program (an order is placed) and other parts want to react (send an e-mail, update the stock, write an audit record). The code that raises the event should not have to contain all that work. There are two classic ways to wire it up. In the Observer pattern the source keeps a list of listeners and calls each one. In publish–subscribe the source hands the event to a broker, and the broker passes it on to whoever subscribed. The page runs the same OrderPlaced events through both at once: Observer on the left, Pub/Sub on the right.

Left: a Subject box with an observers list and arrows straight to Email, Stock and Audit, labelled update(e) as direct calls. Right: a Publisher sends e to a Broker with a topic orders and one queue per subscription; the broker delivers to Email, Stock and Audit. The publisher has no arrow to any subscriber.
The subject holds a reference to every observer and calls it; the publisher only knows the broker and a topic name.

The model on the page: a normal handler takes 1 tick, a slow one 5 ticks. On the right the broker acks a publish after 1 tick and delivers 1 tick later. Each side has its own clock t that starts at 0 when the event is announced.

The Observer pattern

The subject keeps a list of observers. An observer registers with attach(this) and leaves with detach(this). When something changes, the subject calls notify(), which is just a loop:

void notify(Event e) {
    for (Observer o : observers) {
        try { o.update(e); }            // a direct call, on this thread
        catch (Exception ex) { log(ex); }
    }
}

It is simple and fast: no extra process, no copies of the event. But look at what it implies. The subject knows its observers (it holds references to them), the calls happen one after another on the subject's thread, and nothing is stored. Java's listeners (addActionListener), the DOM's addEventListener and C#'s event all work this way. (Demo 1: subscribe and publish: with three observers, notify(e1) returns at t=3.)

Publish–subscribe

In publish–subscribe the publisher sends a message to a named topic on a broker and waits only for the broker's ack. A subscriber sends SUBSCRIBE orders to the broker, which creates a subscription with its own queue. For each message the broker puts a copy in every subscription's queue and delivers it. The subscriber acks after its handler, and only then does the broker remove the copy.

Publisher and subscribers never meet. Neither knows how many of the others there are, they do not have to run at the same time, and they do not wait for each other. RabbitMQ (a fanout exchange with one queue per consumer), Google Cloud Pub/Sub, Amazon SNS with SQS queues, and Kafka (with consumer groups and offsets instead of queues) all have this shape. (Demo 1: publish(e1) returns at t=1, and every subscriber has e1 at t=3.)

A slow listener

In Demo 2: a slow observer blocks notify(), Email is slow: 5 ticks per event. Email is first in the observers list, so on the left Stock gets e1 only at t=6, Audit at t=7, and the subject is stuck in notify() until t=7. On the right the publisher is free at t=1, Stock and Audit finish at t=3, and only Email's own queue waits until t=7.

Two timelines, time running down from t=0 to t=7. Observer: the subject calls Email (busy t=0 to 5), then Stock (5 to 6), then Audit (6 to 7); the subject is blocked the whole time. Pub/Sub: the publisher is blocked only until t=1; Stock and Audit are done at t=3, Email at t=7.
A slow observer delays the subject and everyone after it; a slow subscriber delays only itself.

This is why GUI frameworks tell you not to do slow work in a listener: the listener runs on the UI thread and freezes the window. The usual fix is to hand the work to another thread or queue from inside update(), which is a small step toward publish–subscribe.

An offline listener: lost or buffered

In Demo 3: a subscriber offline, the broker buffers, Stock is down while e1 and e2 are announced. On the left its update() throws; the subject catches it and moves on, and nothing keeps the events for Stock. When Stock comes back, it has missed them for good. On the right e1 and e2 wait in Stock's subscription queue. When Stock is back the broker delivers both, in order, and Stock catches up.

ObserverPublish–subscribe
Stock down for e1, e2✗ both lost for Stock✓ buffered in Stock's queue
Stock backgets only new eventsgets e1, e2, then new events
the source while Stock is downcalls it anyway, gets an exceptionunaffected

Buffering has a price: the broker is one more system to run, the buffered data takes space, and delivery is usually at least once. A subscriber that crashes after its handler but before its ack gets the message again, so handlers must be idempotent.

Unsubscribe

Demo 4: unsubscribe detaches Email after e1. On the left, detach() removes it from the list, and e2 goes only to Stock. A forgotten detach() is a classic leak: the subject's list keeps the observer object alive (the lapsed listener problem). On the right, UNSUBSCRIBE deletes the subscription and anything still in its queue; the publisher does not notice either way.

Which one?

ObserverPublish–subscribe
who knows whomthe subject holds a list of its observersboth sides know only the broker and the topic
timingsynchronous: notify() returns after the last observerasynchronous: publish() returns after the broker's ack
a slow listenerblocks the subject and the observers after itdelays only its own queue
a listener that is downmisses the eventgets it later from the buffer
where it runsone process, in memoryacross processes and machines
costa method calla broker, network hops, at-least-once delivery

Inside one process with fast listeners, the Observer pattern is the simple choice. When the listeners live in other services, may be slow or down, or are added by other teams, put a broker in between. Many systems use both: an in-process event bus (an Observer with a registry in the middle) for local events, and a broker for events that leave the service.

See also the Node.js Event Loop, where an EventEmitter calls its listeners synchronously, Observer style, and How Kafka Moves a Message, a broker that keeps the topic as a log and lets each consumer group track its own offset.

See also Push vs Pull Messaging for the two ways a broker hands messages to subscribers (RabbitMQ push vs Kafka pull).

What the page leaves out

Push vs pull observers (passing the event vs letting observers query the subject), weak references against the lapsed listener leak, event buses and mediators in one process, message filtering and routing keys, durable vs non-durable subscriptions, retries and dead-letter queues, delivery order across partitions, exactly-once delivery, back-pressure when a queue grows, and replaying old messages from a log.