Skip to content

Observer Pattern

Introduction

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It is the foundational pattern behind event-driven systems, reactive streams, and publish-subscribe architectures. Without it, a subject that needs to inform other components of state changes must maintain direct references to every interested party — an arrangement that collapses under its own coupling as the system grows.

Intent

Define a subscription mechanism that allows multiple observer objects to be notified automatically when a subject object changes state, without the subject knowing the concrete types of its observers.

Structure

Participants

ParticipantRole
Subject (Subject interface)Maintains the list of observers and defines subscribe/unsubscribe operations.
ConcreteSubject (OrderEventPublisher)Holds state and fires notification events when state changes.
Observer (OrderEventListener interface)Declares the update interface that all observers must implement.
ConcreteObservers (EmailNotificationListener, InventoryListener, AnalyticsListener)React to notifications and perform their specific side effects.
Event (OrderEvent)Carries the data observers need; used in the push model.

Anti-Pattern — Hardcoded Side-Effect Chain

The naive approach has the Order entity call each downstream service directly. Every new reaction to an order status change requires modifying the Order class itself — a clear violation of the Open/Closed Principle and the Single Responsibility Principle.

java
// ANTI-PATTERN: Order knows about every downstream system
public class Order {

    private String orderId;
    private String status;
    private final EmailService emailService;
    private final InventoryService inventoryService;
    private final AnalyticsService analyticsService;

    public Order(String orderId,
                 EmailService emailService,
                 InventoryService inventoryService,
                 AnalyticsService analyticsService) {
        this.orderId = orderId;
        this.emailService = emailService;
        this.inventoryService = inventoryService;
        this.analyticsService = analyticsService;
        this.status = "PENDING";
    }

    public void updateStatus(String newStatus) {
        String previous = this.status;
        this.status = newStatus;

        // Direct coupling to every interested party:
        emailService.sendStatusUpdate(orderId, newStatus);         // ← must modify Order to add/remove
        inventoryService.onOrderStatusChanged(orderId, newStatus); // ← must modify Order to add/remove
        analyticsService.trackStatusTransition(orderId, previous, newStatus); // ← same

        // Adding a PushNotificationService means opening this class again.
    }
}

Problems:

  • Order is coupled to every system that cares about its state. Adding a fourth service requires reopening and re-testing Order.
  • Order is untestable in isolation — constructing it requires all three service implementations.
  • Removing a listener at runtime is impossible without structural code changes.

Correct Implementation

Event and Listener Contracts

java
import java.time.Instant;

// Immutable event carries all context observers need (push model)
public record OrderEvent(
    String orderId,
    String customerId,
    String previousStatus,
    String newStatus,
    Instant occurredAt
) {}
java
// Observer contract — decoupled from the subject
@FunctionalInterface
public interface OrderEventListener {
    void onOrderEvent(OrderEvent event);
}

Publisher (ConcreteSubject)

java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class OrderEventPublisher {

    // CopyOnWriteArrayList is safe for concurrent iteration while add/remove happen
    private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>();
    private final String orderId;
    private final String customerId;
    private String status;

    public OrderEventPublisher(String orderId, String customerId) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.status = "PENDING";
    }

    public void subscribe(OrderEventListener listener) {
        listeners.add(listener);
    }

    public void unsubscribe(OrderEventListener listener) {
        listeners.remove(listener);
    }

    public void updateStatus(String newStatus) {
        String previous = this.status;
        this.status = newStatus;

        OrderEvent event = new OrderEvent(
            orderId, customerId, previous, newStatus, Instant.now()
        );
        notifyListeners(event);
    }

    private void notifyListeners(OrderEvent event) {
        for (OrderEventListener listener : listeners) {
            try {
                listener.onOrderEvent(event);
            } catch (Exception ex) {
                // Isolate failures: one bad listener must not suppress others
                System.err.printf("Listener %s failed for event %s: %s%n",
                    listener.getClass().getSimpleName(), event.orderId(), ex.getMessage());
            }
        }
    }
}

Concrete Observers

java
public class EmailNotificationListener implements OrderEventListener {

    private final EmailService emailService;

    public EmailNotificationListener(EmailService emailService) {
        this.emailService = emailService;
    }

    @Override
    public void onOrderEvent(OrderEvent event) {
        if ("SHIPPED".equals(event.newStatus())) {
            emailService.sendShipmentConfirmation(event.customerId(), event.orderId());
        } else if ("CANCELLED".equals(event.newStatus())) {
            emailService.sendCancellationNotice(event.customerId(), event.orderId());
        }
    }
}
java
public class InventoryListener implements OrderEventListener {

    private final InventoryService inventoryService;

    public InventoryListener(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }

    @Override
    public void onOrderEvent(OrderEvent event) {
        if ("CONFIRMED".equals(event.newStatus())) {
            inventoryService.reserveItems(event.orderId());
        } else if ("CANCELLED".equals(event.newStatus())) {
            inventoryService.releaseItems(event.orderId());
        }
    }
}
java
public class AnalyticsListener implements OrderEventListener {

    private final AnalyticsService analyticsService;

    public AnalyticsListener(AnalyticsService analyticsService) {
        this.analyticsService = analyticsService;
    }

    @Override
    public void onOrderEvent(OrderEvent event) {
        analyticsService.trackTransition(
            event.orderId(), event.previousStatus(), event.newStatus(), event.occurredAt()
        );
    }
}

Wiring and Usage

java
public class ObserverDemo {
    public static void main(String[] args) {
        OrderEventPublisher order = new OrderEventPublisher("ORD-99", "CUST-7");

        // Register observers — Order itself never changes when we add more
        order.subscribe(new EmailNotificationListener(new EmailService()));
        order.subscribe(new InventoryListener(new InventoryService()));
        order.subscribe(new AnalyticsListener(new AnalyticsService()));

        // Adding a push notification observer requires zero modification to Order
        order.subscribe(event -> System.out.printf(
            "[Push] Order %s is now %s%n", event.orderId(), event.newStatus()
        ));

        order.updateStatus("CONFIRMED");
        order.updateStatus("SHIPPED");
    }
}

Push Model vs. Pull Model

The implementation above uses the push model: the subject packages all relevant data into the event and forwards it to each observer. The pull model sends a minimal notification and lets the observer call back to the subject to retrieve what it needs.

Push ModelPull Model
CouplingObserver depends on event structureObserver depends on subject query API
LatencyLower — one tripHigher — two trips
SelectivityObserver receives everythingObserver fetches only what it needs
Stale dataNonePossible between notification and fetch

Prefer push when the event data is small and most observers need all of it. Prefer pull when observers need highly selective slices of a large subject state.

Pitfall — Memory Leaks from Underegistered Observers

If observers are added but never removed, the subject holds a strong reference to them and prevents garbage collection. This is especially dangerous when observers have short lifetimes (e.g., UI components, request-scoped beans).

java
// PITFALL: this listener is registered but never removed
// The OrderEventPublisher will hold it alive indefinitely
OrderEventListener temporaryListener = event -> System.out.println("Temporary: " + event.orderId());
order.subscribe(temporaryListener);
// ... temporaryListener goes out of scope in calling code, but order still holds the reference

Mitigation — Weak References

java
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;

public class WeakReferencePublisher {

    private final List<WeakReference<OrderEventListener>> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(OrderEventListener listener) {
        listeners.add(new WeakReference<>(listener));
    }

    private void notifyListeners(OrderEvent event) {
        listeners.removeIf(ref -> {
            OrderEventListener listener = ref.get();
            if (listener == null) {
                return true; // GC has collected it — prune the dead reference
            }
            listener.onOrderEvent(event);
            return false;
        });
    }
}

The trade-off: with weak references the caller must hold a strong reference to the listener for as long as it wants to receive events — otherwise the GC may collect it between notifications. Explicit unsubscribe calls are almost always cleaner and more predictable.

Real-World Examples

java.util.Observer (deprecated since Java 9)

java.util.Observer and java.util.Observable were the original JDK incarnation. They were deprecated because Observable is a class (preventing other inheritance), its notify mechanism is not thread-safe, and there is no event type — observers always receive a raw Object. The interface-based approach shown above avoids all three problems.

PropertyChangeListener

java.beans.PropertyChangeListener is the JDK's typed observer for JavaBeans. It uses a PropertyChangeEvent carrying the property name, old value, and new value — a push model with structured metadata.

java
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class OrderBean {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private String status = "PENDING";

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }

    public void setStatus(String newStatus) {
        String old = this.status;
        this.status = newStatus;
        support.firePropertyChange("status", old, newStatus);
    }
}

Spring ApplicationEvent / ApplicationListener

Spring's event infrastructure is Observer backed by the application context. Publishing an event decouples the producer from all listeners registered in the context.

java
// Event
public class OrderStatusChangedEvent extends ApplicationEvent {
    private final OrderEvent orderEvent;

    public OrderStatusChangedEvent(Object source, OrderEvent orderEvent) {
        super(source);
        this.orderEvent = orderEvent;
    }

    public OrderEvent getOrderEvent() { return orderEvent; }
}

// Publisher
@Service
public class OrderService {
    private final ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void updateStatus(String orderId, String newStatus) {
        // ... persist status change ...
        publisher.publishEvent(new OrderStatusChangedEvent(this,
            new OrderEvent(orderId, "CUST-1", "PENDING", newStatus, Instant.now())
        ));
    }
}

// Listener — registered automatically by Spring
@Component
public class AuditListener implements ApplicationListener<OrderStatusChangedEvent> {
    @Override
    public void onApplicationEvent(OrderStatusChangedEvent event) {
        System.out.println("Auditing: " + event.getOrderEvent());
    }
}

AWS SNS Fan-Out

AWS SNS is Observer at the infrastructure level. A single Publish call to an SNS topic delivers the message to all subscribed SQS queues, Lambda functions, HTTP endpoints, and email addresses. Adding a new subscriber requires no change to the publishing service.

Reactive Streams — Publisher / Subscriber

The Reactive Streams spec (java.util.concurrent.Flow.Publisher / Flow.Subscriber) is Observer with back-pressure. The subscriber controls the rate of notifications by calling subscription.request(n), preventing a fast subject from overwhelming a slow observer.

java
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class ReactiveObserverDemo {
    public static void main(String[] args) throws InterruptedException {
        SubmissionPublisher<OrderEvent> publisher = new SubmissionPublisher<>();

        publisher.subscribe(new Flow.Subscriber<>() {
            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription sub) {
                this.subscription = sub;
                sub.request(Long.MAX_VALUE); // request all items (unbounded)
            }

            @Override
            public void onNext(OrderEvent event) {
                System.out.println("Reactive observer received: " + event.orderId());
                subscription.request(1);
            }

            @Override public void onError(Throwable t) { t.printStackTrace(); }
            @Override public void onComplete() { System.out.println("Stream complete"); }
        });

        publisher.submit(new OrderEvent("ORD-1", "CUST-1", "PENDING", "CONFIRMED", Instant.now()));
        Thread.sleep(100);
        publisher.close();
    }
}

When to Use

  • A change in one object requires updating an unknown or variable number of other objects, and you do not want those objects tightly coupled to the subject.
  • An object should notify other objects without assumptions about who those objects are.
  • You need to support broadcast communication — one event, many recipients.
  • You are implementing an event-driven or reactive architecture.

Consequences

Benefits

  • Decouples subjects from observers. The subject publishes events; it has no knowledge of what consumes them.
  • Supports the Open/Closed Principle: new observers can be added without modifying the subject.
  • Enables runtime composition of behaviors — observers can be added and removed dynamically.
  • Maps naturally to event-driven infrastructure (SNS, Kafka, Spring events).

Trade-offs

  • Unexpected updates: a single state change can cascade through many observers in a hard-to-predict order.
  • Update storms: if observers themselves trigger further state changes on the subject, you can enter an infinite notification loop.
  • Memory leaks: underegistered observers keep the subject — and themselves — alive indefinitely (see Weak Reference mitigation above).
  • Ordering is not guaranteed: unless you impose it explicitly, observers fire in registration order, which is an implementation detail callers should not rely on.
  • Debugging difficulty: the indirect notification path makes it harder to trace which observer reacted to which event.
  • Saga Pattern: Choreography-based sagas are built on Observer — each service subscribes to events from other services and reacts to drive the distributed transaction forward.
  • Strategy Pattern: Where Observer decouples who reacts to an event, Strategy decouples how an algorithm executes. The two are often combined: an event triggers the selection of a strategy.
  • Template Method Pattern: Template Method defines a fixed processing skeleton; observer hooks can be inserted at defined steps of that skeleton.
  • Open/Closed Principle: Observer is a primary mechanism for achieving OCP — the subject is closed for modification even as new observers extend its behavioral surface.
  • Clean Architecture: In Clean Architecture, domain events are how use cases communicate across layer boundaries without coupling inner layers to outer ones.