cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 171 of 218
Ideas this builds on
Low-level design171 / 218

Tell interested objects what happened

Picture

Saving a card may interest a counter and a notebook listener. The publisher should announce the event without directly implementing every reaction. Subscribers agree on one callback shape.

Predict

If one listener raises an exception, what happens to the remaining listeners in this design?

Walk through the small example

They still run. publish returns the collected failures. This is an explicit best-effort policy; another application might choose fail-fast behavior instead.

Mental model and complete Python solution

Subscriptions are callbacks. publish takes a snapshot so subscribing or unsubscribing during a notification affects future publications, not the current iteration. unsubscribe is idempotent. A failing listener is recorded and later listeners continue; partial side effects are not rolled back. Everything runs synchronously in the same process, so slow callbacks still delay the publisher. This is not a durable queue, exactly-once delivery, or a retry mechanism.

What the function promises: subscribe(listener) returns an unsubscribe function. Each subscription receives an event once per publication snapshot. publish returns a list of listener exceptions and never hides them as success.

class Events:
    def __init__(self):
        self._listeners = {}
        self._next_token = 0

    def subscribe(self, listener):
        token = self._next_token
        self._next_token += 1
        self._listeners[token] = listener
        def unsubscribe():
            self._listeners.pop(token, None)
        return unsubscribe

    def publish(self, event):
        failures = []
        for listener in tuple(self._listeners.values()):
            try:
                listener(event)
            except Exception as error:
                failures.append(error)
        return failures

events, received = Events(), []
stop = events.subscribe(received.append)
assert events.publish("saved") == []
assert received == ["saved"]
stop()
stop()
events.publish("another")
assert received == ["saved"]
def broken(event):
    raise ValueError("Listener rejected event")
events.subscribe(broken)
events.subscribe(received.append)
assert len(events.publish("still delivered")) == 1
assert received[-1] == "still delivered"

Work and storage: O(number of listeners) dispatch work plus the listeners’ own costs. Snapshot storage is linear in subscriptions.

When to recognize it: The publisher knows an event happened but should not hard-code every reaction to it.

Check before applying it: Delivery order, listener failures, and retries need explicit rules. This in-memory observer is not a durable message queue.

Change one rule

Events must survive a process crash and be retried until handled. Does this callback list suffice?

See what changes

No. Use durable event storage and an explicit delivery protocol, with idempotent consumers or deduplication where needed. The local observer pattern only separates reactions in memory.

Connect

Let small objects work together: Separate event production from individual reactions.

Make failure and retries part of the design: Duplicate delivery is a behavior to decide and test.