Your stock monitoring service tracks a price. Ten different components care when it changes: a risk alert, two portfolio trackers, a chart renderer, an audit log. If the price object calls each one directly, it must know about all ten. Add an eleventh component and you modify the stock object — which should know nothing about UI renderers or audit logs. The Observer pattern inverts this: components register their interest, and the stock object notifies whoever is listening without knowing who that is.
This is the backbone of event-driven programming. Every GUI event listener, every reactive stream, every message bus, every Spring application event — they all implement the same Observer structure. Learning it from first principles makes all of those systems immediately legible.
All code compiles and runs with Java 25. No external dependencies required.
Pattern Structure

Implementation: Stock Price Monitoring
Every subscriber implements one interface, regardless of what it does with the notification.
StockObserver.java
package observer;
/** Observer interface — all subscribers implement this */
public interface StockObserver {
void onPriceChanged(String ticker, double oldPrice, double newPrice);
}
StockMarket is the subject. It holds the observer list and pushes both the old and new price to every subscriber whenever setPrice() is called — it has no idea what any observer does with that data.
StockMarket.java
package observer;
import java.util.ArrayList;
import java.util.List;
/**
* Subject (Observable) — maintains a list of observers and notifies them
* when the stock price changes. Observers register and deregister freely.
*/
public class StockMarket {
private final String ticker;
private double price;
private final List<StockObserver> observers = new ArrayList<>();
public StockMarket(String ticker, double initialPrice) {
this.ticker = ticker;
this.price = initialPrice;
System.out.println("[Market] " + ticker + " initialised at $" + initialPrice);
}
public void subscribe(StockObserver observer) { observers.add(observer); }
public void unsubscribe(StockObserver observer) { observers.remove(observer); }
public void setPrice(double newPrice) {
double old = this.price;
this.price = newPrice;
System.out.printf("[Market] %s price: $%.2f -> $%.2f%n", ticker, old, newPrice);
notifyObservers(old, newPrice);
}
private void notifyObservers(double old, double newPrice) {
for (StockObserver o : observers) {
o.onPriceChanged(ticker, old, newPrice);
}
}
}
Two concrete observers subscribe to the same subject and react completely differently to the same notification — that’s the point of the pattern. AlertObserver only cares about the size of the move:
AlertObserver.java
package observer;
/** Fires an alert when the price changes by more than a threshold % */
public class AlertObserver implements StockObserver {
private final String name;
private final double thresholdPercent;
public AlertObserver(String name, double thresholdPercent) {
this.name = name;
this.thresholdPercent = thresholdPercent;
}
@Override
public void onPriceChanged(String ticker, double oldPrice, double newPrice) {
double change = Math.abs((newPrice - oldPrice) / oldPrice * 100);
if (change >= thresholdPercent) {
System.out.printf(" [ALERT:%s] %s moved %.1f%% — ALERT TRIGGERED%n", name, ticker, change);
} else {
System.out.printf(" [Alert:%s] %s moved %.1f%% — within threshold%n", name, ticker, change);
}
}
}
PortfolioObserver ignores the threshold entirely and just computes a P&L delta for however many shares its owner holds:
PortfolioObserver.java
package observer;
/** Updates portfolio value whenever a held stock changes price */
public class PortfolioObserver implements StockObserver {
private final String ownerName;
private final int sharesHeld;
public PortfolioObserver(String ownerName, int sharesHeld) {
this.ownerName = ownerName;
this.sharesHeld = sharesHeld;
}
@Override
public void onPriceChanged(String ticker, double oldPrice, double newPrice) {
double gain = (newPrice - oldPrice) * sharesHeld;
System.out.printf(" [Portfolio:%s] %s×%d P&L change: %+.2f%n",
ownerName, ticker, sharesHeld, gain);
}
}
Main subscribes all three observers, moves the price twice, then has Bob unsubscribe before a third move — Bob’s portfolio observer never sees that last change.
Main.java
package observer;
/**
* Observer Design Pattern — Runnable Demo
* Run: javac observer/*.java -d out/observer && java -cp out/observer observer.Main
* Article: https://ankurm.com/observer-design-pattern-java/
*/
public class Main {
public static void main(String[] args) {
System.out.println("=== Observer Design Pattern Demo ===\n");
StockMarket aapl = new StockMarket("AAPL", 175.00);
StockObserver alert = new AlertObserver("RiskEngine", 2.0);
StockObserver alice = new PortfolioObserver("Alice", 100);
StockObserver bob = new PortfolioObserver("Bob", 50);
aapl.subscribe(alert);
aapl.subscribe(alice);
aapl.subscribe(bob);
System.out.println();
aapl.setPrice(178.50); // +2% — should trigger alert
System.out.println();
aapl.setPrice(179.00); // small move
System.out.println("\n-- Bob unsubscribes --");
aapl.unsubscribe(bob);
System.out.println();
aapl.setPrice(165.00); // big drop — Bob doesn't hear it
System.out.println("\n=== Demo complete ===");
}
}
Console Output
[Market] AAPL initialised at 5.0
[Market] AAPL price: 5.00 -> 8.50
[ALERT:RiskEngine] AAPL moved 2.0% — ALERT TRIGGERED
[Portfolio:Alice] AAPL×100 P&L change: +350.00
[Portfolio:Bob] AAPL×50 P&L change: +175.00
[Market] AAPL price: 8.50 -> 9.00
[Alert:RiskEngine] AAPL moved 0.3% — within threshold
[Portfolio:Alice] AAPL×100 P&L change: +50.00
[Portfolio:Bob] AAPL×50 P&L change: +25.00
— Bob unsubscribes —
[Market] AAPL price: 9.00 -> 5.00
[ALERT:RiskEngine] AAPL moved 7.8% — ALERT TRIGGERED
[Portfolio:Alice] AAPL×100 P&L change: -1400.00
=== Demo complete ===
Push vs Pull Notification Models
The example above uses the push model: the subject sends both old and new values to each observer. The alternative is the pull model: the subject sends only a reference to itself (or just notifies with no data), and each observer calls back to get what it needs.
// Pull model — observer fetches what it needs from the subject
interface Observer { void update(StockMarket market); }
class PullAlertObserver implements Observer {
@Override
public void update(StockMarket market) {
double price = market.getCurrentPrice(); // observer pulls the data
// ... compute change based on last known price stored in the observer ...
}
}
// Push is simpler when observers need the same data
// Pull is better when different observers need different data from the subject
// or when sending all data upfront would be wasteful
💡 Observer in the JDK: java.beans.PropertyChangeListener and PropertyChangeSupport are the JDK’s built-in Observer implementation for Java beans. Swing’s entire event model — ActionListener, MouseListener, KeyListener — is Observer. Every addXxxListener() / removeXxxListener() method is subscribe() / unsubscribe(). The deprecated java.util.Observable / java.util.Observer pair was removed in Java 9 because Observable was a class (not an interface), making it awkward to compose with other hierarchies.
Thread Safety
The simple ArrayList of observers is not thread-safe. If one thread calls subscribe() while another is iterating through observers in setPrice(), you get ConcurrentModificationException. The standard fix is to copy the list before iterating, or use CopyOnWriteArrayList:
import java.util.concurrent.CopyOnWriteArrayList;
// Thread-safe: reads iterate a snapshot; writes create a new copy
private final List<StockObserver> observers = new CopyOnWriteArrayList<>();
// No synchronisation needed on the notification loop:
observers.forEach(o -> o.onPriceChanged(ticker, old, newPrice));
// CopyOnWriteArrayList.forEach() operates on a consistent snapshot
// even if subscribe/unsubscribe is called concurrently
✅ Avoid Notification Ordering Dependencies: Observers should be independent — the behaviour of observer B should not depend on observer A having been notified first. Java does not guarantee notification order (even with a list, the order depends on registration order, which callers control). If two observers need ordered coordination, use Mediator or Chain of Responsibility instead. Pure Observer assumes subscribers are independent and commutative.
When to Use Observer
Reach for Observer when: a change in one object requires updating others, and you don’t know how many objects need updating or who they are at design time. You want to decouple the source of events from the handling of events. You need dynamic subscription — components connecting and disconnecting at runtime.
Avoid it when: observers have side effects on each other (observer A’s execution affects what observer B sees — this creates ordering dependency). The notification chain is long and deeply nested — hard to debug and profile. You need reliable guaranteed delivery — plain Observer drops notifications if the subject doesn’t hold a reference to a subscriber.
Runnable Code on GitHub
Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/observer/.
javac 03-behavioral/observer/*.java -d out/observer
java -cp out/observer observer.Main
See Also
- Mediator Design Pattern in Java — many-to-many coordination; Observer is one-to-many broadcast from a known subject
- Strategy Design Pattern in Java — also injects behaviour from outside; Strategy swaps algorithms, Observer subscribes to notifications
Further Reading
- Observer — Refactoring.Guru
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma et al., Chapter 5
No Comments yet!