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
- If there's exactly one consumer and it's never going to be more than one, a direct method call is simpler and easier to follow than a subscription mechanism built for a case that doesn't exist yet.
- Observers that must run in a specific order, or whose failure should stop the others from running, don't fit this pattern well — plain Observer makes no ordering or error-isolation guarantees. That needs an explicit pipeline instead.
- Watch for observers that silently keep a reference to a subject alive longer than intended (a classic memory-leak shape in long-lived subjects with short-lived observers) — an observer that's done needs to unsubscribe, not just go out of scope.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — Chapter 5 formalizes Observer.
- Eugster, P. T., Felber, P. A., Guerraoui, R., & Kermarrec, A.-M. (2003). "The Many Faces of Publish/Subscribe." ACM Computing Surveys, 35(2), 114–131. — Observer is the in-process, single-class-boundary special case of the publish/subscribe systems this survey covers; the applied example's webhook/audit/push fan-out is a miniature of exactly what it describes at distributed-systems scale.
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");
}
}