skip to content

When designing notification in modern Java, how do you choose between synchronous listeners (PropertyChangeListener/Swing), the reactive Flow API, and avoiding the deprecated Observable — and what trade-offs drive the choice?

level: principalimportance: nice to knowfreq 30%

answer

  1. Axes: synchrony, cardinality, backpressure, threading, interop, cost
  2. Listeners = simple sync in-VM push, no backpressure
  3. Flow = async streams with request(n) backpressure
  4. Observable = never for new code (class/untyped/setChanged)
  5. Smallest tool that meets the flow-control + synchrony need

basics

~20 s

Use synchronous listeners (PropertyChangeListener, Swing) for simple in-process events where the producer can safely call observers directly. Use the Flow reactive API for asynchronous streams of many items where a slow consumer needs backpressure. Never base new code on the deprecated java.util.Observable.

solid answer

~60 s

The choice hinges on synchrony, cardinality, and rate control. Synchronous listeners (java.beans.PropertyChangeListener, Swing listeners) are a push model: the producer invokes observers inline, on its thread, in order. They're ideal for low-volume, in-VM notifications (a property changed, a button clicked) where simplicity and immediacy win — but they offer no backpressure, can block the producer if a listener is slow, and couple producer and consumer threads. The Flow API is a pull/push hybrid: subscribers signal demand via request(n), giving backpressure so a fast producer can't overwhelm a slow consumer; it's the right tool for asynchronous, possibly-infinite, high-volume streams (I/O, messaging) and for interop with reactive libraries, at the cost of more ceremony and a steeper mental model. The deprecated java.util.Observable is ruled out for new code regardless — it forces inheritance, passes untyped events, and has the error-prone setChanged() protocol. A principal frames it as: smallest tool that meets the synchrony and flow-control needs, biased toward listeners for simple cases and Flow only when backpressure or async truly matter.

go deeper

for a junior

Knows there are listeners and a reactive Flow API, and that the old Observable is deprecated.

for a middle

Can pick listeners for simple in-VM events and Flow for streams, and explain backpressure at a basic level.

for a senior

Compares push listeners vs Flow on synchrony, backpressure, and threading, and justifies excluding Observable.

for a principal

Drives the decision from explicit axes (synchrony/cardinality/flow-control/threading/interop/cost), applies the 'smallest sufficient tool' principle, anticipates threading-contract traps, and weighs ecosystem interop and long-term maintainability.

## The decision you're actually making When one part of a system must react to another, you pick a *notification mechanism*. In modern Java there are three relevant options, and choosing well means matching the mechanism to the problem rather than reaching for the fanciest one. ## The three options, and what each is good at ### 1. Synchronous listeners (push) `java.beans.PropertyChangeListener` and the framework listeners (Swing `ActionListener`, etc.) are the classic **push** model: when state changes, the producer **directly calls** each observer's callback, on the producer's thread, in registration order, and waits for them to return. - **Strengths:** dead simple, immediate, typed, no extra threads or buffering, easy to reason about and test. Composition-based (`PropertyChangeSupport`), so no inheritance trap. - **Weaknesses:** **no backpressure** — a slow or blocking listener stalls the producer (in Swing, it freezes the EDT); the producer and consumer share a thread; not suited to high-volume async streams. - **Use when:** low-volume, in-process events; you control the listeners; synchronous, ordered delivery is fine or desired. ### 2. The Flow reactive API (demand-driven) `java.util.concurrent.Flow` (Publisher/Subscriber/Subscription/Processor) is the reactive-streams contract with **backpressure**: the subscriber asks for items via `request(n)`, so delivery is rate-controlled. - **Strengths:** the consumer governs the rate (no overrun), naturally asynchronous, handles possibly-infinite streams, interoperates with Reactor/RxJava, and serializes signals so subscribers stay single-threaded-simple. - **Weaknesses:** more ceremony (the handshake, terminal-signal rules), a steeper mental model, and Flow itself ships only the SPI interfaces (operators come from libraries). Overkill for a one-shot "property changed" notification. - **Use when:** asynchronous, high-volume or unbounded streams; a slow consumer must throttle a fast producer; you're integrating with reactive infrastructure or non-blocking I/O. ### 3. The deprecated java.util.Observable — don't Ruled out for new code: `Observable` is a class (forces inheritance and burns the single superclass slot), events are untyped `Object`, and the `setChanged()`/`notifyObservers()` protocol is error-prone. It is push-only with weak guarantees — strictly worse than option 1. ## The axes that drive the choice 1. **Synchrony.** Do you want the producer to wait (sync listeners) or to be decoupled in time (Flow)? 2. **Cardinality & lifetime.** A single discrete change favors listeners; a long or unbounded stream favors Flow. 3. **Flow control.** Can a fast producer overwhelm a slow consumer? If yes and that matters, you need backpressure → Flow. 4. **Threading.** Sync listeners run observers on the producer thread (must keep callbacks fast/non-blocking); Flow delivers asynchronously with serialized signals. 5. **Interop & ecosystem.** Need to plug into reactive libraries or non-blocking HTTP? Flow is the lingua franca. 6. **Simplicity / cost.** Listeners are cheaper to write, read, and test; reach for Flow only when its guarantees earn their complexity. ## The principal-level framing The governing principle is **the smallest mechanism that satisfies the synchrony and flow-control requirements**. Bias toward synchronous listeners for ordinary in-VM notifications; escalate to Flow specifically when asynchrony or backpressure is a genuine requirement, not because reactive is fashionable. And never anchor new designs on `java.util.Observable`. A subtle trap is mixing models — e.g. blocking inside a synchronous listener that's on a UI/event thread — so part of the decision is honoring each model's threading contract. ## Deriving your own answer Lay out the axes (synchrony, cardinality, backpressure, threading, interop, cost), map listeners to simple sync cases and Flow to async/backpressured streams, and explicitly exclude the deprecated Observable. Then justify with the 'smallest sufficient tool' principle.

  • Give a concrete case where synchronous listeners are the better choice over Flow.
    A settings object whose 'theme' property changes occasionally and a handful of in-VM components must update. A PropertyChangeListener is immediate, typed, trivial to test, and needs no backpressure. Flow would add a publisher/subscription handshake and async machinery for no benefit.
  • What problem with synchronous listeners specifically motivates Flow's backpressure?
    With push-only listeners, a producer emitting faster than a slow consumer can process leads to unbounded buffering or dropped events, and a slow/blocking listener stalls the producer. Flow's request(n) lets the consumer cap the rate, bounding memory and protecting the producer.

saying these in an interview costs you the question

  • Reaching for Flow for a one-shot property change where a listener suffices
  • Using synchronous listeners for high-volume async streams that need backpressure
  • Recommending java.util.Observable for any new design
  • Blocking inside a synchronous listener on a UI/event thread
  • Assuming reactive is always 'more scalable' regardless of the problem

context