The observer pattern and an event bus

Let a publisher notify many subscribers without knowing who they are.

Python
class EventBus:
    def __init__(self):
        self.handlers = {}

    def subscribe(self, event, handler):
        self.handlers.setdefault(event, []).append(handler)

    def publish(self, event, payload):
        for handler in self.handlers.get(event, []):
            handler(payload)


log = []
bus = EventBus()
bus.subscribe("order_placed", lambda order: log.append(f"email {order}"))
bus.subscribe("order_placed", lambda order: log.append(f"stock {order}"))
bus.publish("order_placed", "#42")
bus.publish("order_cancelled", "#43")
print(log)
Output
['email #42', 'stock #42']

Explanation

A design pattern is a named, reusable solution to a recurring design problem. The observer pattern lets one object announce that something happened while any number of other objects react, without the announcer knowing them. This decouples the code that detects an event from the code that responds to it.

The example is a small event bus. Code registers a handler for an event name with subscribe. When publish is called with that event name, every registered handler is called with the payload. Placing an order can then trigger an email and a stock update without the order code importing either. Adding a third reaction means adding one subscription, not editing the publisher.

The costs are indirection and implicit control flow: it can be hard to see everything that happens after an event. Handlers run in subscription order here, but code should not depend on ordering unless it is part of the contract. A handler that raises an exception can prevent later handlers from running, so production event systems decide how to isolate failures. Unsubscribe when a subscriber is no longer needed to avoid memory leaks.

Explore the dry run
Guided walkthroughReady
1class EventBus:
2 def __init__(self):
3 self.handlers = {}
4
5 def subscribe(self, event, handler):
6 self.handlers.setdefault(event, []).append(handler)
7
8 def publish(self, event, payload):
9 for handler in self.handlers.get(event, []):
10 handler(payload)
11
12
13log = []
14bus = EventBus()
15bus.subscribe("order_placed", lambda order: log.append(f"email {order}"))
16bus.subscribe("order_placed", lambda order: log.append(f"stock {order}"))
17bus.publish("order_placed", "#42")
18bus.publish("order_cancelled", "#43")
19print(log)

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Each event name keeps a list of handlers. subscribe adds one without touching the publisher.—
—
The first subscriber reacts to placed orders by recording an email.—
—
Publishing calls every handler registered for order_placed with the payload #42.—
—
No one subscribed to this event, so nothing happens and no error occurs.—
—
Both handlers ran, in the order they subscribed.—
['email #42', 'stock #42']

Cost and common mistakes

publish is O(h) for h handlers of that event. subscribe is O(1) amortized.

  • Forgetting to unsubscribe keeps objects alive and can leak memory in long-running programs.
  • Relying on handler order, or on one handler's side effects for another, makes event code fragile.
  • Too many events make control flow hard to follow. Use direct calls when only one component needs to respond.

Try it yourself

How would you add an analytics reaction to order_placed?

Reveal answer and reasoning

Call bus.subscribe("order_placed", record_analytics). The publisher and the existing handlers stay unchanged.

Further reading