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.

July 18, 2025·12 min read

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#

ModelDescriptionProsCons
PushSubject sends full data in update()Simple observersObservers may receive unneeded data
PullSubject sends only a reference; observers fetchObservers get only what they needTighter coupling to subject's API

Common Mistakes#

  1. Memory leaks — forgetting to removeObserver creates a retained reference that prevents GC
  2. Notification during notification — modifying the observer list while iterating causes ConcurrentModificationException; use CopyOnWriteArrayList or a snapshot
  3. 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