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.