skip to content

In the Observer pattern, what is the difference between push and pull notification, and what are the trade-offs of each?

level: middleimportance: must knowfreq 62%

answer

  1. push = data travels; pull = signal only
  2. push: self-contained, serializable, snapshot-consistent
  3. pull: minimal interface, observer chooses, glitch risk
  4. hybrid = change descriptor + on-demand read
  5. immutable payloads; no notify on no-op

basics

~20 s

Push means the subject sends the changed data with the notification (update(newValue)). Pull means the notification only says "something changed" and each observer calls back into the subject to read what it needs. Push is efficient but guesses what observers want; pull is flexible but couples observers to the subject's query API.

solid answer

~60 s

Push: `update(event)` carries a payload — the new value, or a change description. The observer needs nothing else, so it need not hold a reference to the subject, the payload can be an immutable value object, and the notification can cross process boundaries because it is self-contained. The risk is that the subject must guess what every observer needs: too little and observers pull anyway, too much and you serialize work nobody consumes. Pull: `update(subject)` or `update()` only signals a change; observers call getters to read current state. The subject interface stays trivial, but every observer now depends on the subject's query surface, N observers cause N read passes, and — critically — an observer may read state that has changed *again* since the notification, so it can act on a value that never corresponded to the event it was told about. In practice most real APIs are hybrid: push a small change descriptor (which property, old/new value, or a delta/change-set) and let the observer pull details on demand.

code

pseudocode · 12 lines
pseudocode
// PUSH: payload is an immutable snapshot of the change
interface Observer { update(Change c) }
record Change(field, oldValue, newValue, sourceId)   // immutable

// PULL: signal only; observer reads what it wants
interface Observer { update(Subject s) }
class Chart { update(s) { render(s.currentPrice()) } }   // may read a NEWER value than announced

// HYBRID (what most real APIs do): push a cheap descriptor, pull the expensive part
interface Observer { update(RowsChanged c) }
record RowsChanged(kind, firstIndex, count, sourceTable)
class Grid { update(c) { if (c.intersects(visibleRange)) fetchAndRender(c.sourceTable, c.firstIndex, c.count) } }

go deeper

for a junior

State the mechanical difference: push passes the new data as an argument, pull passes only a signal (or the subject) and the observer calls getters. Give one example of each.

for a middle

Add trade-offs: push guesses observer needs and can over-send; pull keeps the interface minimal but couples observers to getters and costs N reads. Mention the hybrid change-descriptor style used by real listener APIs.

for a senior

Emphasize consistency and portability: push gives every observer an identical immutable snapshot and can be queued/serialized/replayed across process boundaries; pull can read newer state than announced (a glitch) and needs a live reference that complicates lifetime. Discuss payload immutability and event-schema evolution.

for a principal

Treat it as contract design: the push payload is a published schema with versioning and compatibility obligations; the pull surface is a published query API with the same problem. Choose based on whether the notification will ever leave the process, whether observers must agree on one snapshot, and who bears the cost of change — then codify it as a team convention rather than per-class taste.

## The two shapes When the subject calls its observers, it must decide **how much information travels with the call**. **Push model** — the subject sends the data: ``` interface Observer { update(PriceChanged e) } // e carries symbol, oldPrice, newPrice, timestamp notify(): for o in observers: o.update(PriceChanged(sym, old, new, now)) ``` **Pull model** — the subject sends only a signal: ``` interface Observer { update(Subject s) } // or update() with no arguments at all notify(): for o in observers: o.update(this) // observer body: price = s.getPrice(); if (price != lastSeen) redraw() ``` The original Gang of Four description presents both and calls them the *push model* and the *pull model*; the pull model was described as the more "minimal" subject interface, and the push model as the one that assumes the subject knows observers' needs. ## Push in detail **Advantages** - *Self-contained notification.* The observer receives everything it needs, so it does not need a reference to the subject at all. That is a strictly weaker coupling: the observer depends on an event type, not on a live object with a query API. - *Serializable / relocatable.* Because the event is a value, it can be queued, logged, replayed, put on a message bus, or sent to another process. Pull cannot cross a process boundary — there is nothing to call back into. - *No re-read races.* The payload is a consistent snapshot of the change as of the moment of notification. Even if the subject changes again immediately, the observer still sees the state the event described. - *Efficient for the common case* where every observer wants the same one thing. **Disadvantages** - *The subject must guess.* Designing the event means predicting observer needs. Guess small and observers cast the subject back / call getters anyway; guess big and you compute or serialize data no observer uses (expensive when the payload is a large collection or an eager computed value). - *Event-type churn.* A new observer need often means changing the event class, which means touching the subject — the very coupling Observer set out to remove, now migrated into the payload schema. - *Mutable payload hazard.* Pushing a live reference to an internal collection lets an observer mutate the subject's state, or lets observer #1's edits be visible to observer #2. Push payloads should be immutable, or defensively copied. - *Ordering vs. freshness.* With multiple queued pushes, an observer that processes slowly can be handling event #1 while the world is already at #5 — fine for auditing/replay, wrong for "draw the current value". ## Pull in detail **Advantages** - *Minimal subject-side notification interface* — one `update()` covers every kind of change. - *Observer autonomy.* Each observer reads exactly the fields it cares about; adding an observer with different needs requires no change to the subject or to any event type. - *Naturally coalescing.* If ten changes fired and the observer only reads current state, it converges to the latest value; extra notifications are idempotent redraws. **Disadvantages** - *Coupling to the subject's query API.* Every observer now compiles against getters, so the subject's read surface becomes a public contract that is hard to evolve. Observers may also reach for state that is none of their business. - *Cost.* N observers × M getters, and each getter may recompute. Observers must also figure out *what* changed, often by diffing against a cached copy — logic duplicated in every observer. - *Stale-vs-fresh inconsistency (the "glitch").* Between the notification and the pull, another thread — or a reentrant observer earlier in the loop — may have changed the state. Observer A (called first) may see value v1 and observer B (called later) v2 for what was announced as one event, so views disagree. With push both see the same payload. - *Requires a live reference*, which is exactly the reference that keeps a dead observer's subject-graph reachable and complicates lifetime management. ## The hybrid, which is what real APIs actually do Almost every mature listener API pushes a *small change descriptor* and lets the observer pull details: - Property-change events: `(propertyName, oldValue, newValue, source)` — enough to filter cheaply, plus a `source` handle to pull more. - Collection change events: `(kind = insert/remove/replace, index, count)` — the observer pulls the items only if it is going to render them. - Database/CDC change feeds: `(table, primary key, operation)` and the consumer fetches the row if it needs it. This is the answer most interviewers are looking for: *push what is cheap and identifying, let the observer pull what is expensive or observer-specific.* ## Choosing Prefer **push** when: notification may become asynchronous or cross-process; observers are simple and mostly want the same value; you need each observer to see an identical, consistent snapshot; you want an audit/replay log for free. Prefer **pull** when: observers have widely different, unpredictable needs; the state is large and rarely fully consumed; "latest value wins" semantics are correct and coalescing is desirable; the subject's query API is already public and stable. And in both models: **do not notify when nothing changed** (equality-check first), and **document whether the payload is a snapshot or a live view**.

  • Which model would you pick if the notification might later be delivered over a message queue, and why?
    Push. A queued or cross-process delivery must be self-contained: there is no live subject on the other side to call getters on. A push payload is a serializable value that can be persisted, replayed and audited; pull is only possible when subject and observer share memory.
  • An observer in a pull-model system occasionally renders a value that does not match the event it was notified about. What is happening and how do you fix it?
    Between the notify() call and the observer's getter call, the state changed again (another thread, or a reentrant earlier observer writing back). Fixes: push an immutable snapshot instead of a signal; take the snapshot once before the notification loop and pass it to everyone; or hold a read-consistent version/transaction identifier so observers can pull that specific version.
  • How do you keep a push payload from becoming a dumping ground that every new observer wants to extend?
    Push identity plus a minimal change descriptor (what changed, keys, old/new scalar values) and keep bulk or observer-specific data behind a pull. Version the event type, keep it immutable, and treat additions as an explicit contract change rather than an incidental field.

Push is a newspaper delivered to your door with the story printed in it; pull is a postcard saying "news happened, call the office". The postcard is cheap and lets you ask only what you care about — but if you call an hour later, the story may already have changed, and everyone who calls at a different time hears something different.

saying these in an interview costs you the question

  • Claiming push is 'always more efficient' — it is wasteful when observers use a fraction of a large payload.
  • Claiming pull is 'more decoupled' — pull couples every observer to the subject's getter surface and requires a live reference to it.
  • Pushing a mutable internal collection as the payload, letting observers mutate subject state or see each other's edits.
  • Assuming pull always reads the state the notification described; it reads current state, which may already be newer.
  • Believing you must choose one globally; nearly all real APIs are hybrid (change descriptor + on-demand read).
  • Firing notifications on every setter invocation without checking that the value actually changed.

context