Java Observer Pattern: Learn Event Notifications, Loose Coupling, and Practical Examples

0

Observer Pattern

Imagine you are tracking the status of a cricket match. Instead of repeatedly asking the scoreboard, “Has the score changed?”, you subscribe to match updates. Whenever something important happens, such as a wicket, boundary, or innings change, the system automatically notifies everyone who is interested.

That is the central idea behind the Observer Pattern.

The Observer Pattern defines a one-to-many relationship between objects so that when the state of one object changes, all interested dependent objects are automatically notified.

Important: The Observer Pattern is fundamentally about notification and loose coupling. The object whose state changes does not need to know the detailed implementation of every object interested in its updates.

Why Do We Need the Observer Pattern?

Suppose an application maintains stock prices. Whenever a stock price changes, several parts of the application may need the new value:

  • A dashboard needs to update the displayed price.
  • A mobile notification service may send an alert.
  • A reporting component may record the change.
  • A monitoring system may check threshold conditions.

A tightly coupled implementation might make the stock class directly call every dependent component.

class Stock {

    private Dashboard dashboard;
    private NotificationService notificationService;
    private ReportService reportService;

    public void setPrice(double price) {
        // Update price

        dashboard.update();
        notificationService.sendAlert();
        reportService.record();
    }
}

This creates a maintenance problem. The stock class now knows too much about the objects that consume its changes.

What happens when a new email notification component is added? The stock class must change again.

The Observer Pattern moves this dependency management into a subscription model.

Real-World Analogy

Think about subscribing to a YouTube channel.

The channel is the subject. You are an observer.

The channel does not need to individually design a different notification mechanism for every subscriber. It maintains a collection of subscribers and notifies them when a new video is published.

The relationship looks like this:

                +----------------+
                |    Subject     |
                +----------------+
                  /      |      \
                 /       |       \
                v        v        v
        +----------+ +----------+ +----------+
        | Observer | | Observer | | Observer |
        +----------+ +----------+ +----------+

One subject can therefore notify many observers.

Core Participants

The classic Observer Pattern contains four major participants.

  • Subject: Maintains observers and provides methods for subscribing and unsubscribing.
  • Concrete Subject: Maintains the state that observers are interested in.
  • Observer: Defines the notification contract.
  • Concrete Observer: Performs an action when notified.

Basic Java Example

Let us build the pattern from scratch using a simple weather station.

First, define the observer interface.

interface Observer {
    void update(float temperature);
}

Every observer must provide an update() method.

Now define the subject.

interface Subject {

    void registerObserver(Observer observer);

    void removeObserver(Observer observer);

    void notifyObservers();
}

The subject provides the operations required to manage subscriptions.

Creating the Concrete Subject

import java.util.ArrayList;
import java.util.List;

class WeatherStation implements Subject {

    private final List<Observer> observers = new ArrayList<>();
    private float temperature;

    @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(temperature);
        }
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
        notifyObservers();
    }
}

The WeatherStation maintains the current temperature and a list of observers.

Whenever the temperature changes, it notifies all registered observers.

Creating Concrete Observers

class MobileDisplay implements Observer {

    @Override
    public void update(float temperature) {
        System.out.println(
                "Mobile display: " + temperature + "°C");
    }
}

Another observer can represent a dashboard.

class DashboardDisplay implements Observer {

    @Override
    public void update(float temperature) {
        System.out.println(
                "Dashboard: " + temperature + "°C");
    }
}

Using the Observer Pattern

public class Main {

    public static void main(String[] args) {

        WeatherStation station = new WeatherStation();

        Observer mobile = new MobileDisplay();
        Observer dashboard = new DashboardDisplay();

        station.registerObserver(mobile);
        station.registerObserver(dashboard);

        station.setTemperature(30.5f);
        station.setTemperature(31.2f);

        station.removeObserver(mobile);

        station.setTemperature(32.0f);
    }
}

The first temperature update is received by both observers.

After the mobile display is removed, subsequent notifications are delivered only to the remaining observer.

How the Observer Pattern Works

The execution flow is straightforward:

  • An observer subscribes to the subject.
  • The subject stores the observer.
  • The subject's state changes.
  • The subject triggers notification.
  • Each registered observer receives an update.
  • Observers react independently.

The subject does not need to understand what each observer does with the notification.

Remember: The subject knows who to notify, but it should not need to know how each observer responds.

Push Model vs Pull Model

There are two common ways for a subject to provide updated information to observers.

1. Push Model

In the push model, the subject sends the changed data directly to observers.

observer.update(temperature);

The observer receives the relevant value as part of the notification.

2. Pull Model

In the pull model, the subject notifies the observer that something changed, and the observer asks the subject for the information it needs.

observer.update();

float temperature = station.getTemperature();

The pull model can reduce the amount of information pushed into the notification method and allows observers to request only what they need.

Model How It Works Typical Benefit
Push Subject sends changed data Simple and direct notifications
Pull Subject notifies; observer retrieves data Observers control what information they read

Observer Pattern with Event Data

Real applications often need more information than a single value.

Instead of passing many parameters, we can create an event object.

record OrderEvent(
        String orderId,
        String status,
        double amount) {
}

The observer can then receive the event.

interface OrderObserver {
    void onOrderChanged(OrderEvent event);
}

This approach keeps related event information together and makes the notification contract easier to evolve.

A Practical Order Notification Example

import java.util.ArrayList;
import java.util.List;

class OrderService {

    private final List<OrderObserver> observers =
            new ArrayList<>();

    public void subscribe(OrderObserver observer) {
        observers.add(observer);
    }

    public void unsubscribe(OrderObserver observer) {
        observers.remove(observer);
    }

    public void updateOrder(
            String orderId,
            String status,
            double amount) {

        OrderEvent event =
                new OrderEvent(orderId, status, amount);

        for (OrderObserver observer : observers) {
            observer.onOrderChanged(event);
        }
    }
}

An email observer could react to the event:

class EmailOrderObserver implements OrderObserver {

    @Override
    public void onOrderChanged(OrderEvent event) {
        System.out.println(
                "Email sent for order "
                + event.orderId()
                + ": "
                + event.status());
    }
}

A reporting observer can independently process the same event.

class ReportingObserver implements OrderObserver {

    @Override
    public void onOrderChanged(OrderEvent event) {
        System.out.println(
                "Report updated for order "
                + event.orderId());
    }
}

The order service does not need to know anything about email delivery or reporting.

Observer Pattern Promotes Loose Coupling

One of the biggest benefits of the pattern is reduced coupling between the subject and observers.

Without the pattern:

OrderService
    |
    +----> EmailService
    |
    +----> SmsService
    |
    +----> ReportingService
    |
    +----> AnalyticsService

With the Observer Pattern:

                    +----------------+
                    |  OrderService  |
                    +----------------+
                       /    |    |    \
                      /     |    |     \
                     v      v    v      v
                 Observer Observer Observer Observer

The subject depends on the observer abstraction rather than concrete notification implementations.

Adding a New Observer

Suppose we later introduce an SMS notification.

class SmsOrderObserver implements OrderObserver {

    @Override
    public void onOrderChanged(OrderEvent event) {
        System.out.println(
                "SMS sent for order "
                + event.orderId());
    }
}

The existing order service does not need to be modified to understand SMS-specific behavior.

We simply subscribe the new observer.

orderService.subscribe(
        new SmsOrderObserver());

This is one way the pattern supports the Open/Closed Principle.

Observer and the Open/Closed Principle

A well-designed subject can remain unchanged while new observers are added.

The subject is open to new observer implementations without requiring modification to its core notification mechanism.

However, this benefit depends on maintaining a clean abstraction. If the subject begins checking concrete observer types using conditions such as instanceof, much of the advantage is lost.

Observer and the Single Responsibility Principle

The subject's responsibility is to maintain its state and manage notifications.

Each observer handles its own reaction.

For example:

  • Email observer handles email-related behavior.
  • SMS observer handles SMS-related behavior.
  • Analytics observer handles analytics-related behavior.
  • Dashboard observer handles display updates.

This separation keeps individual classes more focused.

Subscription Management

A practical observer implementation needs a reliable subscription mechanism.

The common operations are:

subscribe(observer);
unsubscribe(observer);

Good subscription management matters because an observer that is no longer needed should not continue receiving events.

Otherwise, the system may perform unnecessary work or retain references to objects that should have become eligible for garbage collection.

Duplicate Observer Registration

Consider this code:

subject.registerObserver(observer);
subject.registerObserver(observer);

If the subject uses a normal ArrayList, the same observer may receive the notification twice.

If duplicate registration should not be allowed, a Set can be considered.

private final Set<Observer> observers =
        new HashSet<>();

The choice depends on the application's semantics. Sometimes duplicate subscriptions are meaningful; often they are accidental.

Observer and Thread Safety

In a multithreaded application, observers may subscribe, unsubscribe, and receive notifications concurrently.

A simple collection may therefore not be sufficient for every design.

Potential concerns include:

  • Concurrent modification of the observer collection.
  • An observer unsubscribing while notifications are being delivered.
  • Multiple threads updating subject state.
  • Observers performing slow operations.
  • Ordering of notifications.

Practical Insight: Thread safety should be designed according to the application's concurrency model rather than assuming that every observer implementation is automatically safe.

Synchronous vs Asynchronous Notifications

Observer notifications can be synchronous or asynchronous.

Synchronous Notification

The subject directly invokes the observer.

for (Observer observer : observers) {
    observer.update(event);
}

The subject waits for each observer call to complete.

Asynchronous Notification

The subject or event infrastructure schedules observer processing separately.

executor.submit(() -> observer.update(event));

Asynchronous notification can improve responsiveness, but it introduces additional concerns such as thread management, ordering, retries, failures, and eventual consistency.

Aspect Synchronous Asynchronous
Execution Observer runs during notification Observer runs separately
Caller waits Usually yes Usually no
Complexity Lower Higher
Failure handling Usually immediate Requires explicit asynchronous handling
Typical use Simple in-process updates Background processing and event-driven workflows

Observer Pattern vs Event-Driven Architecture

The Observer Pattern and event-driven architecture are related, but they are not identical.

A traditional Observer implementation is often an in-process relationship between objects. The subject maintains references to observers and invokes them directly.

An event-driven architecture may use a dedicated event broker, message queue, or distributed event infrastructure.

Application
    |
    v
Event Publisher
    |
    v
Event Infrastructure
    |
    +----> Consumer A
    +----> Consumer B
    +----> Consumer C

The architectural scale and delivery guarantees are therefore different, even though the underlying publish-subscribe idea is related.

Observer Pattern vs Mediator

Both patterns help manage communication between objects, but their communication models differ.

Pattern Main Communication Idea
Observer One subject notifies many observers.
Mediator A central mediator coordinates communication among multiple components.

Observer is especially useful when many independent components need to react to a state change.

Observer Pattern vs Pub/Sub

Publish-subscribe systems are conceptually similar because publishers produce notifications and subscribers consume them.

The main distinction is often the degree of decoupling.

In a classic Observer implementation, the subject usually maintains direct references to observers.

In a message-based publish-subscribe system, publishers and subscribers may communicate through an intermediary infrastructure and may not know each other at all.

Common Beginner Mistakes

1. Forgetting to Unsubscribe

Observers that remain registered after they are no longer needed can cause unnecessary notifications and object-lifetime problems.

2. Creating Excessively Large Events

Sending an enormous object graph with every notification can increase coupling and make the event contract difficult to maintain.

3. Making the Subject Know Concrete Observers

Code such as checking whether an observer is an email service, SMS service, or analytics service defeats the purpose of abstraction.

4. Ignoring Observer Failures

If one observer throws an exception, the implementation should have a deliberate policy for whether other observers still receive the event.

5. Performing Slow Work Synchronously

An observer that sends a remote request or performs heavy processing can block the subject when notifications are synchronous.

6. Assuming Notification Order Is Always Guaranteed

If observers are processed asynchronously, completion order may differ from subscription order. Ordering requirements should therefore be explicit.

Best Practices

  • Program against observer interfaces rather than concrete implementations.
  • Provide explicit subscribe and unsubscribe operations.
  • Define a clear notification contract.
  • Keep observers focused on their own reaction logic.
  • Decide explicitly whether notifications should be synchronous or asynchronous.
  • Handle observer failures deliberately.
  • Consider duplicate subscription behavior.
  • Consider thread safety when state changes or subscriptions can occur concurrently.
  • Avoid passing unnecessary data through events.
  • Use a dedicated event infrastructure when the problem grows beyond simple in-process notifications.

A More Robust Notification Design

For larger applications, it can be useful to separate event creation from notification management.

interface EventListener<T> {
    void onEvent(T event);
}

class EventPublisher<T> {

    private final List<EventListener<T>> listeners =
            new ArrayList<>();

    public void subscribe(EventListener<T> listener) {
        listeners.add(listener);
    }

    public void unsubscribe(EventListener<T> listener) {
        listeners.remove(listener);
    }

    public void publish(T event) {
        for (EventListener<T> listener : listeners) {
            listener.onEvent(event);
        }
    }
}

This generic approach can support different event types without creating a completely separate publisher implementation for every event.

Where the Observer Pattern Is Useful

The pattern is useful whenever multiple components need to react to changes without tightly coupling the source of the change to every consumer.

Common examples include:

  • User interface updates.
  • Stock price notifications.
  • Order-status changes.
  • Inventory updates.
  • Application monitoring.
  • Configuration changes.
  • Domain events.
  • Notification systems.
  • In-process event handling.

When Should You Avoid the Observer Pattern?

Observer is not automatically the right choice for every notification requirement.

Consider alternatives when:

  • There is only one consumer and direct communication is clearer.
  • The communication relationship is highly complex and requires centralized coordination.
  • Reliable asynchronous delivery is required across processes or services.
  • The notification chain is becoming difficult to trace.
  • The application requires sophisticated retry, persistence, or delivery guarantees better provided by dedicated messaging infrastructure.

Testing Observer-Based Systems

Testing should verify both subscription behavior and notification behavior.

Useful test cases include:

  • A subscribed observer receives the expected event.
  • An unsubscribed observer no longer receives events.
  • Multiple observers receive the same event.
  • Duplicate registration behaves according to the design.
  • One observer failing does not unintentionally prevent required notifications.
  • Concurrent operations behave correctly when concurrency is supported.

Interview Insight: What Is the Observer Pattern?

The Observer Pattern defines a one-to-many dependency in which multiple observers subscribe to a subject and are automatically notified when the subject's state changes. It promotes loose coupling between the state owner and the components reacting to that state.

Interview Insight: What Is the Difference Between Push and Pull Models?

In the push model, the subject sends the changed data to the observer during notification.

In the pull model, the subject sends a notification and the observer retrieves the required state from the subject.

Interview Insight: Does Observer Always Mean Asynchronous Processing?

No.

A traditional Observer implementation is often synchronous. The subject directly calls each observer.

Asynchronous processing can be added, but it introduces additional concurrency and failure-handling considerations.

Interview Insight: How Does Observer Support Loose Coupling?

The subject depends on an observer abstraction instead of concrete observer implementations. New observers can be added without requiring the subject to know their internal behavior.

Interview Insight: What Happens If an Observer Throws an Exception?

There is no universal answer. The design should explicitly define the failure policy.

For example, the publisher may:

  • Stop notification immediately.
  • Catch the failure and continue notifying other observers.
  • Record the failure and process it separately.

For critical event systems, failure handling should be designed rather than left to accidental behavior.

Quick Learning Checkpoint

Before moving on, make sure you can answer these questions:

  • What problem does the Observer Pattern solve?
  • What are the Subject and Observer roles?
  • Why does the subject depend on an observer abstraction?
  • What is the difference between push and pull notification?
  • Why is unsubscribe important?
  • How can asynchronous observers change the design?
  • How is Observer different from Mediator?
  • How is a traditional Observer implementation different from distributed publish-subscribe?

Quick Revision

Concept Key Idea
Observer Pattern Notifies multiple dependent objects when a subject changes.
Subject Maintains state and manages observers.
Observer Defines how notifications are received.
Subscribe Adds an observer to the notification list.
Unsubscribe Removes an observer from the notification list.
Push Model Subject sends changed data to observers.
Pull Model Observer retrieves required information after notification.
Synchronous Observer executes during the notification call.
Asynchronous Observer processing happens independently from the notification caller.
Main Benefit Reduces coupling between a state-changing object and its dependents.

Final Takeaway

The Observer Pattern turns direct dependency into a subscription relationship. Instead of a subject knowing exactly which components need to react to every change, it works with an observer abstraction and broadcasts notifications to registered observers.

The pattern is especially useful when one state change can affect many independent components. It supports loose coupling, extensibility, and separation of responsibilities.

The most important idea to remember is simple: one object changes, many interested objects are notified without tightly coupling the change source to every consumer.

Post a Comment

0Comments
Post a Comment (0)