Low Level Design
Observer Pattern — Event-Driven Design in Java
Build a type-safe, decoupled event system using the Observer pattern. Covers the classic GoF design, Java's built-in tools, and a modern generic implementation.
What is the Observer Pattern?#
The Observer (also called Publish-Subscribe) is a behavioral pattern where an object (the Subject / Publisher) maintains a list of dependents (Observers / Subscribers) and notifies them automatically when its state changes.
Use it when:
- A change in one object must trigger updates in multiple others
- You want loose coupling between the event source and its consumers
- The number of subscribers is unknown at compile time (e.g., UI components, plugins)
GoF Class Diagram#
Classic Implementation#
Subject Interface#
java
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
Observer Interface#
java
public interface Observer {
void update(String symbol, double price);
}
StockMarket (Concrete Subject)#
java
import java.util.ArrayList;
import java.util.List;
public class StockMarket implements Subject {
private final List<Observer> observers = new ArrayList<>();
private String symbol;
private double price;
public void setPrice(String symbol, double price) {
this.symbol = symbol;
this.price = price;
notifyObservers(); // push model: subject drives the update
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(symbol, price);
}
}
}
Concrete Observers#
java
public class MobileApp implements Observer {
private final String userId;
public MobileApp(String userId) { this.userId = userId; }
@Override
public void update(String symbol, double price) {
System.out.printf("[MobileApp %s] %s → $%.2f%n", userId, symbol, price);
}
}
public class WebDashboard implements Observer {
@Override
public void update(String symbol, double price) {
System.out.printf("[WebDashboard] Updating chart: %s = $%.2f%n", symbol, price);
}
}
Usage#
java
public class Main {
public static void main(String[] args) {
StockMarket market = new StockMarket();
Observer mobile = new MobileApp("alice");
Observer web = new WebDashboard();
market.registerObserver(mobile);
market.registerObserver(web);
market.setPrice("AAPL", 182.50);
market.setPrice("GOOG", 141.00);
market.removeObserver(mobile);
market.setPrice("AAPL", 183.10); // only WebDashboard notified
}
}
Output:
[MobileApp alice] AAPL → $182.50
[WebDashboard] Updating chart: AAPL = $182.50
[MobileApp alice] GOOG → $141.00
[WebDashboard] Updating chart: GOOG = $141.00
[WebDashboard] Updating chart: AAPL = $183.10
Modern Generic Implementation#
The classic GoF version is tightly coupled to the payload type. A generic event bus decouples this:
java
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class EventBus {
private final Map<Class<?>, List<Consumer<Object>>> listeners =
new HashMap<>();
@SuppressWarnings("unchecked")
public <T> void subscribe(Class<T> eventType, Consumer<T> listener) {
listeners
.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add((Consumer<Object>) listener);
}
public <T> void publish(T event) {
List<Consumer<Object>> eventListeners =
listeners.getOrDefault(event.getClass(), List.of());
for (Consumer<Object> listener : eventListeners) {
listener.accept(event);
}
}
}
Event Records (Java 16+)#
java
public record StockPriceEvent(String symbol, double price) {}
public record NewsEvent(String headline, String source) {}
Wiring it up with lambdas#
java
public class Main {
public static void main(String[] args) {
EventBus bus = new EventBus();
// Subscribe with lambda — no boilerplate class needed
bus.subscribe(StockPriceEvent.class, e ->
System.out.printf("Price alert: %s = $%.2f%n", e.symbol(), e.price()));
bus.subscribe(NewsEvent.class, e ->
System.out.printf("News: \"%s\" via %s%n", e.headline(), e.source()));
bus.publish(new StockPriceEvent("TSLA", 248.73));
bus.publish(new NewsEvent("Fed holds rates", "Reuters"));
}
}
Push vs Pull Model#
| Model | Description | Pros | Cons |
|---|---|---|---|
| Push | Subject sends full data in update() | Simple observers | Observers may receive unneeded data |
| Pull | Subject sends only a reference; observers fetch | Observers get only what they need | Tighter coupling to subject's API |
Common Mistakes#
- Memory leaks — forgetting to removeObserver creates a retained reference that prevents GC
- Notification during notification — modifying the observer list while iterating causes ConcurrentModificationException; use CopyOnWriteArrayList or a snapshot
- Cascading updates — A notifies B which notifies C which notifies A → infinite loop
Key Takeaways#
- Observer gives you loose coupling — the subject knows nothing about the concrete observers
- Prefer the generic EventBus for application-level events (UI, domain events, integration)
- Use CopyOnWriteArrayList if observers can subscribe/unsubscribe during notification
- Java's standard library provides java.util.Observer (deprecated since Java 9) — prefer the custom approach above or a battle-tested library like Guava's EventBus