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.
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.
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.
| Observer | Publish–subscribe | |
|---|---|---|
| Stock down for e1, e2 | ✗ both lost for Stock | ✓ buffered in Stock's queue |
| Stock back | gets only new events | gets e1, e2, then new events |
| the source while Stock is down | calls it anyway, gets an exception | unaffected |
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?
| Observer | Publish–subscribe | |
|---|---|---|
| who knows whom | the subject holds a list of its observers | both sides know only the broker and the topic |
| timing | synchronous: notify() returns after the last observer | asynchronous: publish() returns after the broker's ack |
| a slow listener | blocks the subject and the observers after it | delays only its own queue |
| a listener that is down | misses the event | gets it later from the buffer |
| where it runs | one process, in memory | across processes and machines |
| cost | a method call | a 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.