The Observer Pattern- A pattern that keeps us happy

Discover how the Observer Pattern enables automatic notifications in smartwatches, hospital monitoring systems, and modern applications. Learn about this essential design pattern from the Gang of Four that allows data sources to automatically push updates to interested observers without manual polling.

Do you ever wonder how a smartwatch can notify someone miles away when it detects a dangerous heart rhythm? Or how ICU/hospital monitoring systems stream a patient’s vitals to multiple screens and trigger alerts when something goes abnormal like a high temperature, irregular heartbeat, or dropping oxygen level?

In these situations, we usually don’t manually “call an API” every few seconds to check for updates. Instead, updates are pushed automatically (or continuously streamed) to whoever needs them dashboards, mobile apps, nurses’ stations, alerting systems, etc.

This is exactly the kind of problem the Gang of Four (GoF) described with the Observer Pattern.

What Do These Examples Share?

One thing keeps changing: data.

  • Temperature changes
  • Heart rate changes
  • Oxygen saturation changes
  • Sensor readings change

The Observer Pattern gives the data source (the thing that changes) a clean way to notify all interested parties (watchers) automatically whenever the value changes.

That’s why it’s called Observer: multiple observers “watch” a subject, and when the subject changes, they get notified

What Is the Observer Pattern?

The intent of this pattern is to define a one-to-many dependency between objects so that when one object changes its state, all its dependents are notified and updated automatically.

We have an Observable, which is our source of data or something which keeps changing.
We have many objects which is called Observers.
The update can use a pull or push mechanism.
Let us try to implement this pattern in Simple Java

Pattern Structure: Class Diagram

Here’s the class structure showing how Observer and Observable work together:

Creating the Observer Interface

In languages like java, the power comes from the interfaces. Like that we have Observer.

interface Observer<T> {
void update(T newValue);
}

The interface has an update() function that gets called when there is a change in state.

The Observable Base Class

abstract class Observable<T> {
    private final List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    // Protected: only subclasses can trigger notifications
    protected void notifyObservers(T value) {
        for (Observer o : observers) {
            o.update(T);
        }
    }
}

The abstract class Observable has three methods , we can add more methods
addObserver(Observer observe) – This will add list of objects who are subscribing
removeObserver(Observer observe) – This will remove list of object who are subscribing
notifyObservers(T value) – This method sends the updates

The TemperatureSensor Observable

class TemperatureSensor extends Observable<Integer>{
    private Integer temperature;

    public void setTemperature(Integer newTemp) {
        this.temperature = newTemp;
        notifyObservers(newTemp); // notify everyone when it changes
    }
}

The method set Temperature is called new temperature is notified to subscribing objects.

Putting It All Together: Demo

public class ObserverDemo {
    public static void main(String[] args) {
        TemperatureSensor sensor = new TemperatureSensor();

        // Observer 1: Dashboard
        sensor.addObserver(newTemp ->
            System.out.println("Dashboard shows temperature: " + newTemp)
        );

        // Observer 2: Alert system
        sensor.addObserver(newTemp -> {
            if (newTemp > 50) {
                System.out.println("ALERT! Temperature too high: " + newTemp);
            }
        });

        sensor.setTemperature(40);
        sensor.setTemperature(55);
    }
}

 

Common Pitfalls and Challenges

While the Observer Pattern is powerful, it comes with several challenges you should be aware of:

Memory Leaks

If observers are not explicitly removed when they’re no longer needed, they remain in the observer list. This prevents garbage collection and leads to memory leaks. Always call removeObserver() when an observer is done listening.

Unpredictable Notification Order

There’s no guaranteed order in which observers are notified. If one observer depends on another observer’s action, this can lead to bugs. The pattern doesn’t provide any mechanism to control execution order.

Performance Bottlenecks

Notifying many observers synchronously can block the main thread, especially if each observer performs time-consuming operations. In our basic implementation, all notifications happen on the same thread, which can cause lag in responsive applications.

Lack of Thread Safety

Our implementation isn’t thread-safe. If multiple threads add/remove observers or trigger notifications simultaneously, you can get ConcurrentModificationException or other threading issues. Consider using CopyOnWriteArrayList or synchronized blocks for multi-threaded environments.

Cascading Updates

An observer’s update method might modify another observable, which then notifies its observers, creating a chain reaction. Without careful design, this can lead to infinite loops or hard-to-debug behavior.

When to Use the Observer Pattern

The Observer Pattern shines in these scenarios:

  • Event-driven systems: GUI applications where buttons, forms, and components need to respond to user actions
  • Real-time data monitoring: Stock tickers, sensor dashboards, live sports scores
  • MVC architecture: Views observing model changes to update UI automatically
  • Pub/Sub messaging: When multiple components need to react to state changes without tight coupling

When NOT to Use the Observer Pattern

Avoid this pattern in these situations:

  • Simple one-to-one relationships: If you only have one subscriber, direct method calls are simpler.
  • Simple one-to-one relationships: If you only have one subscriber, direct method calls are simpler.
  • Order-dependent operations: When the sequence of notifications matters, the Observer pattern isn’t ideal.
  • High-frequency updates: If state changes thousands of times per second, the overhead can be significant.
  • Performance-critical systems: The indirection and iteration over observers adds latency.
  • Complex dependencies: When observers depend on each other, consider other patterns like mediator.

Real-World Implementations

The Observer Pattern is everywhere in modern software development:

  • Java
    • java.util.Observable and Observer (deprecated in Java 9)
    • Spring Framework’s ApplicationEventPublisher
  • JavaScript
    • DOM event listeners (addEventListener)
    • Node.js EventEmitter
    • RxJS Observables
    • Vue.js and Angular reactivity systems
  • Android
    • LiveData for lifecycle-aware observables
    • Kotlin Flow and StateFlow
  • Modern Alternatives
    • Reactive Extensions (RxJava, RxJS, RxSwift)
    • Google Guava’s EventBus
    • Apache Kafka for distributed event streaming

Thread Safety Considerations

Our basic implementation has a critical flaw: it’s not thread-safe. Here’s how to fix it

abstract class ThreadSafeObservable {
// Use CopyOnWriteArrayList for thread-safe operations
private final List> observers = new CopyOnWriteArrayList<>();
public void addObserver(Observer<T> o) {
    observers.add(o);
}

public void removeObserver(Observer<T> o) {
    observers.remove(o);
}

protected void notifyObservers(T value) {
    // CopyOnWriteArrayList ensures thread-safe iteration
    for (Observer<T> o : observers) {
        o.update(value);
    }
}
}

Why CopyOnWriteArrayList?

Thread-safe for concurrent reads and writes
No ConcurrentModificationException during iteration
Best for scenarios where reads are more frequent than writes each modification creates a new copy of the underlying array

Best Practices

Follow these guidelines to use the Observer Pattern effectively:

1. Always Clean Up Observers

Always remove observers when they’re no longer needed to prevent memory leaks. Use try-finally blocks or lifecycle methods to ensure cleanup.

public class Dashboard implements Observer {
private TemperatureSensor sensor;

public void start(TemperatureSensor sensor) {
    this.sensor = sensor;
    sensor.addObserver(this);
}

public void stop() {
    if (sensor != null) {
        sensor.removeObserver(this);
    }
}

@Override
public void update(Integer temp) {
    System.out.println("Temperature: " + temp);
}
}

2. Handle Errors in Observers

One failing observer shouldn’t break the entire notification chain. Wrap update calls in try-catch blocks.

3. Consider Asynchronous Notifications

For long-running observers, use ExecutorService to notify asynchronously and avoid blocking the observable.

4. Document Your Observer Contract

Clearly specify whether observers can modify the observable during updates, whether notifications are synchronous or async, and what thread safety guarantees you provide.

5. Use Weak References for Long-Lived Observables

If your observable lives much longer than observers, consider using WeakReference to allow garbage collection of observers that are no longer in use.

srnyapathi
srnyapathi
Articles: 41