Design Patterns in Java

Observer Pattern

15 min Lesson 6 of 13

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects: when one object (the subject or publisher) changes state, all of its dependents (observers or listeners) are notified automatically. This is the foundational pattern behind every event-driven system you have ever used — from Swing listeners and Android callbacks to Spring application events and reactive streams.

The Problem It Solves

Without Observer, a subject that needs to notify others must hold hard-coded references to every interested party. Add a new consumer and you modify the subject — a clear violation of the Open/Closed Principle. Observer decouples the producer of events from the consumers, so each side can evolve independently.

Publish / Subscribe vocabulary: "Subject/Observer" is the classic GoF terminology. Modern systems often use "Publisher/Subscriber" (or "EventEmitter/Listener"). The mechanics are identical; the naming reflects the domain. In Java you will encounter both forms in the same codebase.

Classic GoF Structure in Java

The minimal contract requires two interfaces and one concrete subject:

// The observer contract public interface Observer { void update(String eventType, Object payload); } // The subject contract public interface Subject { void addObserver(Observer o); void removeObserver(Observer o); void notifyObservers(String eventType, Object payload); }

A concrete subject stores its observers in a list and fans out each notification:

import java.util.ArrayList; import java.util.List; import java.util.Objects; public class EventBus implements Subject { private final List<Observer> observers = new ArrayList<>(); @Override public void addObserver(Observer o) { Objects.requireNonNull(o, "Observer must not be null"); observers.add(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers(String eventType, Object payload) { // Iterate over a snapshot to avoid ConcurrentModificationException // if an observer removes itself during notification List.copyOf(observers).forEach(o -> o.update(eventType, payload)); } }
Always iterate over a snapshot. Calling List.copyOf(observers) before the loop prevents ConcurrentModificationException when an observer unregisters itself in reaction to being called — a very common real-world scenario.

A Realistic Example: Order Processing

Imagine an e-commerce service where placing an order must trigger multiple side effects — sending a confirmation email, updating inventory, and writing an analytics event. With Observer each concern registers independently:

// Strongly-typed event record (Java 16+) public record OrderEvent(long orderId, String status) {} // Three independent observers public class EmailNotifier implements Observer { @Override public void update(String eventType, Object payload) { if ("ORDER_PLACED".equals(eventType) && payload instanceof OrderEvent oe) { System.out.println("Sending confirmation email for order " + oe.orderId()); } } } public class InventoryService implements Observer { @Override public void update(String eventType, Object payload) { if ("ORDER_PLACED".equals(eventType) && payload instanceof OrderEvent oe) { System.out.println("Reserving stock for order " + oe.orderId()); } } } public class AnalyticsTracker implements Observer { @Override public void update(String eventType, Object payload) { System.out.println("Analytics: event=" + eventType + " payload=" + payload); } }
// Wiring it together EventBus bus = new EventBus(); bus.addObserver(new EmailNotifier()); bus.addObserver(new InventoryService()); bus.addObserver(new AnalyticsTracker()); // Publishing an event bus.notifyObservers("ORDER_PLACED", new OrderEvent(42L, "PLACED")); // Console output: // Sending confirmation email for order 42 // Reserving stock for order 42 // Analytics: event=ORDER_PLACED payload=OrderEvent[orderId=42, status=PLACED]

The OrderService that calls notifyObservers knows nothing about email, inventory, or analytics. Add a new side effect by writing a new class — zero changes to existing code.

Type-Safe, Topic-Filtered Event Bus

Production systems rarely use a single observer list. A common improvement maps event types to separate listener lists, avoiding the instanceof dance inside every observer:

import java.util.*; import java.util.function.Consumer; public class TypedEventBus { private final Map<Class<?>, List<Consumer<?>>> listeners = new HashMap<>(); public <T> void subscribe(Class<T> eventType, Consumer<T> listener) { listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener); } @SuppressWarnings("unchecked") public <T> void publish(T event) { List<Consumer<?>> targets = listeners.getOrDefault(event.getClass(), List.of()); List.copyOf(targets).forEach(l -> ((Consumer<T>) l).accept(event)); } }
TypedEventBus bus = new TypedEventBus(); // Lambda observers — no boilerplate interface implementations bus.subscribe(OrderEvent.class, e -> System.out.println("Email for order " + e.orderId())); bus.subscribe(OrderEvent.class, e -> System.out.println("Inventory for order " + e.orderId())); bus.publish(new OrderEvent(99L, "PLACED"));

Using Consumer<T> with lambdas removes the need to write separate observer classes for simple reactions — much closer to how modern Java frameworks handle events internally.

Java's Built-in Observer Support

Java has shipped Observer support in several forms over the years:

  • java.util.Observer / java.util.Observable — deprecated since Java 9. Avoid in new code: thread safety is broken and the design is inflexible.
  • java.beans PropertyChangeSupport — still used in desktop/Swing code for property-change notifications.
  • Flow API (java.util.concurrent.Flow) — Java 9+ reactive streams: Publisher, Subscriber, Subscription, Processor. Back-pressure aware; the modern foundation for reactive programming.
  • Spring ApplicationEvent — the Spring Framework wraps Observer behind ApplicationEventPublisher / @EventListener, giving you declarative, async-capable event handling with DI.
Never use java.util.Observable. It was deprecated in Java 9 for good reasons: the notification is not thread-safe, and its design couples observers to a concrete base class rather than an interface. Use the Flow API, a purpose-built event bus, or a framework like Spring Events instead.

Threading and Memory Considerations

Two issues catch engineers off guard in production:

  1. Thread safety. If observers are registered or removed from multiple threads, the list must be guarded. Replace ArrayList with CopyOnWriteArrayList for a lock-free, thread-safe alternative that naturally solves the snapshot problem too.
  2. Memory leaks. A subject that holds strong references to observers keeps them alive indefinitely. UI frameworks (Swing, JavaFX, Android) are notorious for this: registering a listener in an activity/fragment and never unregistering it holds the entire view hierarchy in memory. Always pair addObserver with a corresponding removeObserver in a lifecycle teardown method.
import java.util.concurrent.CopyOnWriteArrayList; public class ThreadSafeEventBus implements Subject { // CopyOnWriteArrayList: thread-safe reads, snapshot-on-write private final CopyOnWriteArrayList<Observer> observers = new CopyOnWriteArrayList<>(); @Override public void addObserver(Observer o) { observers.addIfAbsent(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers(String type, Object payload) { // CopyOnWriteArrayList iterator is already a snapshot — no copy needed observers.forEach(o -> o.update(type, payload)); } }

Trade-offs and When to Use Observer

  • Use it when multiple unrelated components must react to state changes and you want to keep the publisher ignorant of its consumers.
  • Watch out for cascade notification storms — an observer that triggers another event can create hard-to-debug chains. Log event dispatches in development.
  • Prefer typed buses or frameworks (Spring Events, Guava EventBus, RxJava) over hand-rolled string-keyed buses in production — they add compile-time safety and async support.
  • Consider reactive streams (Project Reactor, RxJava) when you need back-pressure, composition, or complex async pipelines; Observer by itself is fire-and-forget.

Summary

The Observer pattern is the engine of event-driven architecture. Its essence is a contract between a publisher that does not know who is listening and any number of listeners that do not know about each other. In modern Java this manifests as lambda-based event buses, the Flow API, and framework-level abstractions like Spring Events. The key professional skills are: keeping the observer list thread-safe, preventing memory leaks through disciplined unregistration, and knowing when to reach for a higher-level reactive abstraction instead of rolling your own bus.