Observer

June 17, 2026 in patterns 4 minutes

A intermediate-level guide to Observer: before-and-after java code and diagrams for a CS student.

The Burden of Tight Coupling

Imagine you are building a weather station. When the temperature changes, you need to update a digital display, log the data to a file, and perhaps trigger an alarm if it gets too hot. At first, this seems easy: you just call display.update() and logger.log() inside your sensor class.

However, as you add more features—like a mobile app notification or a web dashboard—your sensor class begins to bloat. It must now “know” about every single component that needs the temperature data. This is known as tight coupling: the TemperatureSensor cannot exist or be tested without also bringing along its specific dependents. If you change how the logger works, you have to modify the sensor.

class TemperatureSensor {
    private double temperature;

    public void setTemperature(double temperature) {
        this.temperature = temperature;
        // Tight coupling: Sensor must know about every specific dependent
        updateLabel();
        logToConsole();
    }

    private void updateLabel() {
        System.out.println("UI Label updated to: " + temperature);
    }

    private void logToConsole() {
        System.out.println("Log entry: Temp is " + temperature);
    }
}

In this design, the sensor has a direct dependency on concrete implementations like a UI label and a console logger. It is doing too much work; it is not just sensing temperature, it is managing its own list of dependents.

  classDiagram
    class TemperatureSensor {
        -double temperature
    }
    class UpdateLabelMethod {
    }
    class LogToConsoleMethod {
    }
    TemperatureSensor ..> UpdateLabelMethod : calls
    TemperatureSensor ..> LogToConsoleMethod : calls

The Intuition: A Subscription Model

Think of a YouTube channel. When a creator uploads a new video, they do not manually call every single subscriber on the phone to tell them about it. Instead, the creator simply “broadcasts” the update to a list of subscribers.

The creator does not know if you are watching on a phone or a laptop, nor do they care what you do with the video after you watch it. They only care that you have joined their subscription list. This is the essence of the Observer pattern: a “Subject” maintains a list of “Observers” and notifies them when its state changes.

Decoupling With the Observer Pattern

To fix the coupling in our sensor, we introduce an interface. An interface acts as a contract; it tells the sensor what can be done without telling it how to do it. Instead of the sensor knowing about specific classes like Label or Logger, it only knows about the TemperatureObserver interface.

import java.util.ArrayList;
import java.util.List;

interface TemperatureObserver {
    void onTemperatureChanged(double newTemperature);
}

class TemperatureSensor {
    private final List<TemperatureObserver> observers = new ArrayList<>();
    private double temperature;

    public void addObserver(TemperatureObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(TemperatureObserver observer) {
        observers.remove(observer);
    }

    public void setTemperature(double temperature) {
        this.temperature = temperature;
        notifyObservers();
    }

    private void notifyObservers() {
        for (TemperatureObserver observer : observers) {
            observer.onTemperatureChanged(temperature);
        }
    }
}

By using an interface, we have achieved several things:

  1. Decoupling: The TemperatureSensor no longer knows which specific classes are listening to it. It only knows they implement onTemperatureChanged.
  2. Extensibility: You can add a new SmsAlertObserver without ever touching the code inside TemperatureSensor. This follows the Open-Closed Principle: classes should be open for extension but closed for modification.

To see how this works during execution, observe the sequence of events when a temperature change occurs:

  sequenceDiagram
    participant S as TemperatureSensor
    participant O1 as LabelObserver
    participant O2 as LoggerObserver

    S ->> S: setTemperature(25.0)
    S ->> O1: onTemperatureChanged(25.0)
    O1 ->> O1: updateDisplay()
    S ->> O2: onTemperatureChanged(25.0)
    O2 ->> O2: writeToFile()

When to Use It, and When Not To

The Observer pattern is ideal for implementing distributed event-handling systems or building reactive UIs where a change in data must reflect across multiple disparate components simultaneously.

However, use it with caution:

  • Complexity: It can make the flow of a program harder to follow because the connection between the subject and the observer is established at runtime rather than compile time.
  • The Cascading Update Trap: If an Observer reacts to a change by calling a method on the Subject that triggers another change, you can accidentally create an infinite loop that crashes your application.
  • Push vs. Pull: In our example, we used a “Push” model where the sensor passes the temperature directly through the method argument. While simple, if the data being passed is huge (like a whole database record), it can lead to unnecessary overhead. Sometimes it is cleaner to let the Observer receive a notification and then “Pull” only the specific data it needs from the Subject.

Takeaways

  • Use the Observer pattern to decouple a Subject from its dependents.
  • Always depend on an interface (the Observer) rather than a concrete class.
  • A Subject should manage a collection of observers, allowing for dynamic registration and removal at runtime.

Usage

Using the Observer pattern to attach multiple decoupled dependents

TemperatureSensor sensor = new TemperatureSensor();
sensor.addObserver(temp -> System.out.println("UI: " + temp));
sensor.addObserver(temp -> System.out.println("Log: " + temp));
sensor.setTemperature(25.5);