Skip to main content

Push vs Pull, and the Pub/Sub Generalization

What This Concept Is

Observer is one-to-many notification inside a process. Two small design choices and one scaling move shape how it is actually used:

  • Push model: the subject passes the updated value (or event object) to each observer.
  • Pull model: the subject passes itself; observers read whatever they need.
  • Pub/Sub: subject and subscriber are mediated by a broker or bus; they do not know each other's identity at all. Topics (or channels) route events.

Strictly speaking Pub/Sub is architectural, while Observer is a pattern at object granularity. The shape is the same; the coupling is looser, and the boundary can be a network.

Why It Matters Here

Knowing the axis lets you pick the smallest structure that solves the problem:

  • Two objects in the same process, few events? Observer, push.
  • Many observers that need different slices of the state? Observer, pull.
  • Events that cross modules, teams, or services? Pub/Sub with explicit topics.

Leaping to a full event bus when two subscribe calls would do is over-engineering. Sticking to direct Observer when new teams keep asking for "just add me as a listener" is under-engineering.

Concrete Example

Topic-based pub/sub in Python, ~30 lines:

from collections import defaultdict
from typing import Callable, Any

class EventBus:
def __init__(self):
self._topics: dict[str, list[Callable[[Any], None]]] = defaultdict(list)

def subscribe(self, topic: str, fn: Callable[[Any], None]) -> Callable[[], None]:
self._topics[topic].append(fn)
return lambda: self._topics[topic].remove(fn)

def publish(self, topic: str, payload: Any) -> None:
for fn in list(self._topics.get(topic, [])):
try:
fn(payload)
except Exception as e:
print(f"listener on {topic!r} failed: {e}")

bus = EventBus()
off = bus.subscribe("order.placed", lambda ev: print("email:", ev["id"]))
bus.subscribe("order.placed", lambda ev: print("metrics:", ev["id"]))
bus.publish("order.placed", {"id": 42})
off()

Publishers and subscribers never see each other. Routing is by topic string. This is the seed from which enterprise buses grow.

Push vs pull for the weather example:

// push
interface Observer { void update(double tempC, double humidity); }

// pull
interface Observer { void update(WeatherStation src); } // reads getTempC(), etc.

Push is smaller when all observers want the same values. Pull is better when observers vary in what they need, or when the payload is large.

Common Confusion / Misconception

  • Treating a Pub/Sub bus as magic decoupling. Subscribers still depend on the shape of the payload and the topic name. If those change, every consumer breaks. Decoupled types, not decoupled contracts.
  • Topic-sprawl. A hundred ad-hoc topic names with no schema becomes worse than the direct coupling it replaced. Treat topic names and payload shapes as an API.
  • Confusing async with pub/sub. A pub/sub call can be synchronous (in-process bus) or asynchronous (message broker). These are independent choices.
  • Assuming delivery guarantees. In-process buses deliver unless the process crashes. Network brokers have choices (at-most-once, at-least-once, exactly-once-ish). Know which you have.

How To Use It

Decision questions when designing notification:

  1. Scope. Same object graph? Use Observer. Cross-module or cross-service? Use Pub/Sub.
  2. Payload size. Small? Push. Large or variable per observer? Pull.
  3. Broker. In-process dispatcher is enough until events must survive a restart or cross a network.
  4. Contract. Name the event type. Describe it once (a class, a schema, a TypeScript type) and reuse.
  5. Backpressure. If publishers can overwhelm subscribers, you have left Observer territory and must think about queues or streaming (reactive libraries like RxJS, Project Reactor, Akka Streams).

Check Yourself

  1. What does push pass that pull does not?
  2. When does topic-based pub/sub stop being "just Observer with extra steps"?
  3. What does "a decoupled type, not a decoupled contract" mean?
  4. What kind of failure mode appears with network-based pub/sub that an in-process Observer does not have?

Mini Drill or Application

Take a desktop app's file watcher and three consumers: a UI panel, a backup service, and a search indexer.

  1. Model first with direct Observer, push model, one subject.
  2. Now add a fourth consumer from a different module and note what changed in the subject's imports.
  3. Refactor to a small topic-based bus (file.changed, file.deleted). Show that the subject no longer mentions consumers at all.
  4. Write a one-paragraph memo on when the first design was fine and why the second is only worth it here.

Read This Only If Stuck