← All patterns

Observer

Behavioral · view source on GitHub

Read this in: English · Português · Español

Category: Behavioral

The problem

One object's state change needs to be reflected in several others, but those others shouldn't be hard-wired into the object that changed. Calling each dependent directly from inside the subject couples it to every consumer's concrete type, and adding a new consumer means editing the subject's code again. What's needed is a way for interested parties to register themselves and be notified, without the subject knowing anything about them beyond a common interface.

The solution

The subject keeps a list of observers behind a common interface and notifies all of them whenever its state changes; each observer decides independently what to do with that notification. Subscribing and unsubscribing don't require touching the subject's own logic.

classDiagram
    class Subject {
        -observers
        +subscribe(o)
        +unsubscribe(o)
        +notifyObservers()
    }
    class Observer {
        <<interface>>
        +update(state)
    }
    class ConcreteObserverA
    class ConcreteObserverB
    Subject o-- Observer
    Observer <|.. ConcreteObserverA
    Observer <|.. ConcreteObserverB

Classic example

classic/WeatherStation is the canonical example: a subject that pushes temperature/humidity readings to every subscribed WeatherObserver. CurrentConditionsDisplay just stores the latest reading; HeatAlertObserver derives a boolean alert from it — two observers doing genuinely different things with the exact same notification, neither aware the other exists. WeatherStationTest covers both observers reacting independently to one measurement, an unsubscribed observer no longer receiving updates, and the heat alert clearing once the temperature drops back down.

Applied example: transaction status fan-out

applied/TransactionStatusPublisher notifies three independent observers whenever a transaction's status changes: WebhookNotifierObserver (records an outbound webhook call), AuditLogObserver (logs every transition for compliance), and PushNotificationObserver (only reacts to the terminal states, SETTLED/FAILED — a customer doesn't need a push for every intermediate state). This is exactly the shape a real payment gateway needs when a transaction's lifecycle has to reach several independent systems: the publisher doesn't know or care how many consumers exist, or what any of them actually do with the notification. TransactionStatusPublisherTest covers all three observers reacting to a full PENDING→PROCESSING→SETTLED sequence, the push observer also firing on FAILED, and an unsubscribed observer no longer receiving updates.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :behavioral:observer:jacocoTestReport

Report at behavioral/observer/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/behavioral/observer/classic/WeatherStationTest.java
package com.designpatterns.behavioral.observer.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class WeatherStationTest {

    @Test
    void everySubscribedObserverReactsIndependentlyToTheSameMeasurement() {
        WeatherStation station = new WeatherStation();
        CurrentConditionsDisplay display = new CurrentConditionsDisplay();
        HeatAlertObserver alert = new HeatAlertObserver();
        station.subscribe(display);
        station.subscribe(alert);

        station.setMeasurements(36.5, 40.0);

        assertThat(display.currentConditions()).isEqualTo("Temp: 36.5C, Humidity: 40.0%");
        assertThat(alert.isAlertActive()).isTrue();
    }

    @Test
    void anUnsubscribedObserverStopsReceivingUpdates() {
        WeatherStation station = new WeatherStation();
        CurrentConditionsDisplay display = new CurrentConditionsDisplay();
        station.subscribe(display);
        station.setMeasurements(20.0, 50.0);

        station.unsubscribe(display);
        station.setMeasurements(30.0, 60.0);

        assertThat(display.currentConditions()).isEqualTo("Temp: 20.0C, Humidity: 50.0%");
    }

    @Test
    void theHeatAlertClearsWhenTheTemperatureDropsBackDown() {
        WeatherStation station = new WeatherStation();
        HeatAlertObserver alert = new HeatAlertObserver();
        station.subscribe(alert);

        station.setMeasurements(40.0, 30.0);
        assertThat(alert.isAlertActive()).isTrue();

        station.setMeasurements(22.0, 30.0);
        assertThat(alert.isAlertActive()).isFalse();
    }
}
src/test/java/com/designpatterns/behavioral/observer/applied/TransactionStatusPublisherTest.java
package com.designpatterns.behavioral.observer.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class TransactionStatusPublisherTest {

    @Test
    void everyObserverReceivesEveryStatusChangeButOnlyPushNotifiesOnTerminalStates() {
        TransactionStatusPublisher publisher = new TransactionStatusPublisher();
        WebhookNotifierObserver webhook = new WebhookNotifierObserver();
        AuditLogObserver audit = new AuditLogObserver();
        PushNotificationObserver push = new PushNotificationObserver();
        publisher.subscribe(webhook);
        publisher.subscribe(audit);
        publisher.subscribe(push);

        publisher.publish("tx-1", TransactionStatus.PENDING);
        publisher.publish("tx-1", TransactionStatus.PROCESSING);
        publisher.publish("tx-1", TransactionStatus.SETTLED);

        assertThat(webhook.deliveredWebhooks()).containsExactly(
                "tx-1:PENDING", "tx-1:PROCESSING", "tx-1:SETTLED"
        );
        assertThat(audit.entries()).containsExactly(
                "transaction tx-1 moved to PENDING",
                "transaction tx-1 moved to PROCESSING",
                "transaction tx-1 moved to SETTLED"
        );
        assertThat(push.pushedMessages()).containsExactly("Your transaction tx-1 is settled");
    }

    @Test
    void pushNotifiesOnFailedToo() {
        TransactionStatusPublisher publisher = new TransactionStatusPublisher();
        PushNotificationObserver push = new PushNotificationObserver();
        publisher.subscribe(push);

        publisher.publish("tx-2", TransactionStatus.FAILED);

        assertThat(push.pushedMessages()).containsExactly("Your transaction tx-2 is failed");
    }

    @Test
    void anUnsubscribedObserverStopsReceivingStatusChanges() {
        TransactionStatusPublisher publisher = new TransactionStatusPublisher();
        AuditLogObserver audit = new AuditLogObserver();
        publisher.subscribe(audit);
        publisher.publish("tx-3", TransactionStatus.PENDING);

        publisher.unsubscribe(audit);
        publisher.publish("tx-3", TransactionStatus.SETTLED);

        assertThat(audit.entries()).containsExactly("transaction tx-3 moved to PENDING");
    }
}

View full JaCoCo coverage report →