Observer Pattern: Reacting to Changes

java java21 scala scala2 scala3 kotlin design-patterns behavioral-patterns observer-pattern

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:

  • MarketTicker knows about Dashboard, AlertService, and AnalyticsReporter
  • 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:

  1. a dashboard that redraws the chart
  2. a notifications service that sends mobile alerts
  3. 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?
It creates a one-to-many relationship between a subject and its dependents. When the subject changes, it notifies all registered observers without the subject needing to know the exact type of each consumer. That makes the producer and consumers loosely coupled, which is usually a good thing.
How is the Observer pattern different from callbacks?
A callback is usually a single function passed to another function for one-time handling. The Observer pattern is a broader mechanism: many listeners can subscribe, unsubscribe, and receive future events from the same subject. In that sense, callbacks are a building block, while Observer is a reusable event-listener architecture.
What are the common pitfalls with observers?
The classic problems are stale listeners, memory leaks, and notification storms. If a consumer forgets to unsubscribe, the subject may keep a reference to it long after it should be gone. If too many observers react to every update, performance can degrade quickly. That is why lifecycle management and batching matter in production systems.
How do modern reactive frameworks relate to the Observer pattern?
Frameworks such as RxJava, Reactor, Kotlin Flow, and ZIO build on the same core idea: a source emits values and downstream consumers react to them. They add stronger typing, back-pressure, composition, and scheduling. In other words, modern reactive systems are more sophisticated observers, not a completely different concept.
When should I choose Observer over a simple method call?
Use Observer when the relationship is inherently one-to-many and the publisher should not need to know every consumer. If one component just needs to ask another component to do a single job, a direct method call is simpler. If many components need to react to the same event over time, Observer is usually the cleaner design.

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:


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.