Imagine your trading dashboard needs to refresh when prices change, your alert service wants a push notification, and the analytics worker wants a timestamped snapshot. You could make the price source aware of all of them individually, but then every new consumer becomes a special case. The Observer pattern gives you one clean rule: when the subject changes, it tells everyone who subscribed.
The Problem: One source, many listeners
A stock ticker is a textbook Observer problem. The source state changes often, while the consumers want to react only when something relevant happens. A brittle implementation would do something like this:
MarketTickerknows aboutDashboard,AlertService, andAnalyticsReporter- every new consumer requires a new branch in the price-update logic
- if a consumer forgets to unsubscribe, you get stale listeners and memory leaks
This is exactly the sort of coupling that makes code hard to test and harder to extend.
Key Concepts
| Concept | In this example | Why it matters |
|---|---|---|
| Subject | MarketTicker |
Owns the state and publishes updates |
| Observer | PriceObserver |
Reacts to updates without the source knowing details |
| Event payload | PriceUpdate |
Carries the changed data in one immutable object |
| Lifecycle | subscribe() / unsubscribe() |
Keeps the system clean and avoids stale listeners |
Real Use Case: Live Price Alerts
Think about a trading app with three subscribers:
- a dashboard that redraws the chart
- a notifications service that sends mobile alerts
- a risk engine that recalculates exposure
The ticker does not need to know which type of consumer each observer is. It just emits a new PriceUpdate and lets the listeners decide what to do with it.
The Design: Subject + Observer + Event
The important part is the contract between the subject and the observers:
- the subject keeps a list of subscribers
- each subscriber receives the same update object
- the update is immutable, so consumers can react without racing the source
The Solution: Observer Across JVM Languages
This example uses a MarketTicker that stores the latest value and notifies all registered observers when a new price arrives.
public final class MarketTicker {
public record PriceUpdate(String symbol, BigDecimal price, Instant timestamp) {}
@FunctionalInterface
public interface PriceObserver {
void onPriceUpdate(PriceUpdate update);
}
private final Set<PriceObserver> observers = ConcurrentHashMap.newKeySet();
public void subscribe(PriceObserver observer) {
observers.add(observer);
}
public PriceUpdate publishPrice(String symbol, BigDecimal price) {
PriceUpdate update = new PriceUpdate(symbol, price, Instant.now());
observers.forEach(observer -> observer.onPriceUpdate(update));
return update;
}
}
class MarketTicker {
data class PriceUpdate(
val symbol: String,
val price: BigDecimal,
val timestamp: Instant
)
fun interface PriceObserver {
fun onPriceUpdate(update: PriceUpdate)
}
private val observers = mutableSetOf<PriceObserver>()
fun subscribe(observer: PriceObserver) {
observers += observer
}
fun publishPrice(symbol: String, price: BigDecimal): PriceUpdate {
val update = PriceUpdate(symbol, price, Instant.now())
observers.toList().forEach { it.onPriceUpdate(update) }
return update
}
}
class MarketTicker {
case class PriceUpdate(symbol: String, price: BigDecimal, timestamp: Instant)
trait PriceObserver {
def onPriceUpdate(update: PriceUpdate): Unit
}
private val observers = mutable.Set.empty[PriceObserver]
def subscribe(observer: PriceObserver): Unit = observers += observer
def publishPrice(symbol: String, price: BigDecimal): PriceUpdate = {
val update = PriceUpdate(symbol, price, Instant.now())
observers.foreach(_.onPriceUpdate(update))
update
}
}
class MarketTicker:
case class PriceUpdate(symbol: String, price: BigDecimal, timestamp: Instant)
trait PriceObserver:
def onPriceUpdate(update: PriceUpdate): Unit
private val observers = mutable.Set.empty[PriceObserver]
def subscribe(observer: PriceObserver): Unit = observers += observer
def publishPrice(symbol: String, price: BigDecimal): PriceUpdate =
val update = PriceUpdate(symbol, price, Instant.now())
observers.foreach(_.onPriceUpdate(update))
update
When Observers Cause Trouble
The pattern is powerful, but it has a few common traps:
- forgetting to unsubscribe leads to stale listeners and memory leaks
- a noisy subject can trigger expensive work for every subscriber
- invoking observers while holding internal locks can create deadlocks
- callbacks can accidentally re-enter the subject and create recursive update loops
This is why real systems often combine Observer with explicit lifecycle rules, batching, or back-pressure. The pattern itself is not the problem; the careless execution is.
Comparison Table
| Concern | Java 21 | Scala 2/3 | Kotlin |
|---|---|---|---|
| Subject contract | interface + set of listeners | trait + mutable set | fun interface + mutable set |
| Event payload | record | case class | data class |
| Subscription lifecycle | subscribe() / unsubscribe() |
same | same |
| Typical use | Swing, listeners, event buses | reactive streams, domain events | Flow, coroutine-based streams |
| Main risk | stale callbacks | mutable state + side-effects | leaking coroutine scopes |
Best Practices
- Keep the observer callback small and side-effect free when possible.
- Prefer immutable payloads so consumers can safely read a snapshot.
- Implement explicit unsubscribe paths for long-lived objects.
- Consider weak references or managed lifecycles when the subject outlives the observers.
- Be careful with notification storms; if many listeners exist, batch updates or debounce them.
Interview Q&A: Observer Pattern in Practice
What is the Observer pattern really doing?
How is the Observer pattern different from callbacks?
What are the common pitfalls with observers?
How do modern reactive frameworks relate to the Observer pattern?
When should I choose Observer over a simple method call?
Conclusion
The Observer pattern is one of the clearest examples of a one-to-many dependency in real software. If you are coming from Scala, think of it as a publisher that pushes immutable events to registered listeners. If you are coming from Java, think of it as a well-defined event contract that avoids direct coupling between the producer and each consumer.
The real win is not just the callback list. It is the separation of concerns: the subject knows how to publish state changes, while the observers decide what to do with them.
Code Samples
All examples in this post are runnable. Find them in the repository:
- Java 21 implementation
- Java 21 tests
- Kotlin implementation
- Kotlin tests
- Scala 2 implementation
- Scala 2 tests
- Scala 3 implementation
- Scala 3 tests
This post is part of the Design Patterns in JVM Languages - Your Guide to the Top 10. Nearby related posts from the same guide: Strategy Pattern: Choosing Algorithms at Runtime and Facade Pattern: Simplifying Complex Systems.