Observer Design Pattern in Java: Real-World Example and Complete Guide

Observer Design Pattern in Java: Real-World Example and Complete Guide

Observer Design Pattern in Java: Real-World Example and Complete Guide

Imagine you have placed an online order. A few seconds later, you receive an order confirmation email. When the order is shipped, you receive another update. Your order tracking page also shows the latest status.

Have you ever wondered how one order-status change can trigger several different actions without the order service being tightly coupled to every notification component?

This is where the Observer Design Pattern in Java becomes useful. It allows one object to notify multiple interested objects whenever a relevant event or state change occurs.

In this article, we will implement a realistic e-commerce order notification system using plain Java. We will also explore an original SVG diagram, Spring Boot events, advantages and disadvantages, and important Java interview questions.

1. What Is the Observer Design Pattern?

The Observer pattern is a behavioral design pattern that defines a one-to-many relationship between objects. When the subject changes its state or publishes an event, its registered observers are notified.

The subject does not need to know the detailed implementation of each observer. It only needs a common contract through which it can notify interested objects.

For example, an order service can publish an order-status update. An email handler, SMS handler, and dashboard handler can each react to that update independently.

In simple words: One object announces a change, and all registered interested objects get an opportunity to react.

Main participants

  • Subject or Publisher: Maintains registered observers and sends notifications.
  • Observer: Defines the method used to receive updates.
  • Concrete Observer: Implements a specific reaction to an event.
  • Client: Connects the subject and observers during application setup.

2. Real-World Example: E-Commerce Order Tracking

Consider a shopping application where a customer places an order. During its lifecycle, the order may move through several statuses:

  1. PLACED: The order has been created.
  2. CONFIRMED: The order has been accepted.
  3. SHIPPED: The order has been handed over for delivery.
  4. DELIVERED: The order has reached the customer.

Different parts of the application may care about these changes:

  • The email notification component sends an order update.
  • The SMS component sends a short status message.
  • The tracking dashboard refreshes the displayed status.
  • An audit component records a business event when required.

Without the Observer pattern, the order service could end up directly calling each of these components. Adding a new observer would then require changing the order service.

With the Observer pattern, the order service publishes the status change to its registered observers. Each observer decides what to do with the event.

This keeps order-status management separate from the different reactions to that status. The basic publish-and-notify relationship is the defining idea of the Observer pattern. Read more about the pattern and its uses.

3. The Problem Without the Observer Pattern

Suppose the order service directly depends on every notification component:

public class OrderService {

    private EmailService emailService;
    private SmsService smsService;
    private DashboardService dashboardService;

    public void updateStatus(String orderId, String status) {
        // Update order status

        emailService.sendUpdate(orderId, status);
        smsService.sendUpdate(orderId, status);
        dashboardService.refresh(orderId, status);
    }
}

This code is easy to understand initially. However, the order service now needs to know about several unrelated reactions to a status change.

What happens as the application grows?

  • A new notification channel requires another dependency.
  • Testing order-status changes becomes harder because notification dependencies must be configured.
  • Notification logic may become mixed with core order-processing logic.
  • Changes in notification implementations can affect the order service.

Direct calls are not always bad design. If the workflow is small and stable, they may be the clearest option. The Observer pattern becomes valuable when several independently managed components need to react to the same event.

4. How the Observer Pattern Works

The general execution flow has five steps:

  1. The client creates the subject and the observers.
  2. The client registers the observers with the subject.
  3. The subject changes its state or publishes an event.
  4. The subject notifies its registered observers.
  5. Each observer performs its own action.

The subject knows that observers implement a particular interface, but it does not need to know whether an observer sends an email, updates a dashboard, or writes an audit record.

Important design decision

Decide what an observer receives. It might receive the subject itself, the current state, or an event object containing the relevant data. For a business application, an immutable event object is often easier to understand and test than exposing the entire subject.

5. Observer Design Pattern Diagram

This original diagram shows how a single order-status publisher notifies several observers through a shared interface.

Observer Design Pattern in Java An order service publishes an order status event to an observer interface. Email, SMS, and dashboard observers receive the event and perform their respective actions. Order Service Publishes status change OrderObserver Common contract notify EmailObserver Sends email update SmsObserver Sends SMS update DashboardObserver Refreshes status view One event source, multiple independently implemented reactions
Figure: The Observer pattern lets multiple registered components react to an order-status event without the order service directly depending on each concrete observer.

6. Complete Java Implementation: Order Status Notifications

Let's build a runnable example using plain Java. The implementation uses custom interfaces rather than Java's deprecated java.util.Observer and java.util.Observable.

Create the following files in the same package:

  • OrderStatus.java
  • OrderEvent.java
  • OrderObserver.java
  • OrderService.java
  • EmailObserver.java
  • SmsObserver.java
  • DashboardObserver.java
  • ObserverPatternDemo.java

Step 1: Define the order statuses

public enum OrderStatus {
    PLACED,
    CONFIRMED,
    SHIPPED,
    DELIVERED
}

An enum is preferable to arbitrary status strings because it limits the status to a defined set of values and helps prevent spelling mistakes.

Step 2: Create an immutable OrderEvent

import java.time.Instant;
import java.util.Objects;

public final class OrderEvent {

    private final String orderId;
    private final OrderStatus status;
    private final Instant occurredAt;

    public OrderEvent(String orderId, OrderStatus status) {
        this.orderId = Objects.requireNonNull(orderId);
        this.status = Objects.requireNonNull(status);
        this.occurredAt = Instant.now();
    }

    public String getOrderId() {
        return orderId;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public Instant getOccurredAt() {
        return occurredAt;
    }
}

The event carries the information observers need. Making the fields final helps prevent an observer from accidentally changing the event data that other observers receive.

Step 3: Define the observer interface

public interface OrderObserver {

    void onOrderStatusChanged(OrderEvent event);
}

Every observer must implement the same notification method. The order service can therefore notify different implementations through one common contract.

Step 4: Implement the subject — OrderService

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

public class OrderService {

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

    public void addObserver(OrderObserver observer) {
        observers.add(Objects.requireNonNull(observer));
    }

    public boolean removeObserver(OrderObserver observer) {
        return observers.remove(observer);
    }

    public void updateStatus(
            String orderId,
            OrderStatus newStatus) {

        OrderEvent event = new OrderEvent(orderId, newStatus);

        System.out.println(
                "Order " + orderId
                + " status changed to " + newStatus
        );

        // Notify the observers registered at this moment.
        for (OrderObserver observer :
                new ArrayList<>(observers)) {
            observer.onOrderStatusChanged(event);
        }
    }
}

The order service stores registered observers. Whenever the status changes, it creates an event and notifies them.

The snapshot created using new ArrayList<>(observers) allows an observer to register or unregister observers during a callback without modifying the list being traversed. This is a simple single-threaded design; it does not make registration and notification thread-safe.

Step 5: Implement EmailObserver

public class EmailObserver implements OrderObserver {

    @Override
    public void onOrderStatusChanged(OrderEvent event) {
        System.out.println(
                "[EMAIL] Order " + event.getOrderId()
                + " is now " + event.getStatus()
        );
    }
}

In a real application, this class could call an email service or enqueue an email request. The example prints a message so that the pattern can be tested without external services.

Step 6: Implement SmsObserver

public class SmsObserver implements OrderObserver {

    @Override
    public void onOrderStatusChanged(OrderEvent event) {
        System.out.println(
                "[SMS] Order " + event.getOrderId()
                + " status: " + event.getStatus()
        );
    }
}

This observer reacts to the same event but performs a different action. The order service does not need to know how SMS delivery works.

Step 7: Implement DashboardObserver

public class DashboardObserver implements OrderObserver {

    @Override
    public void onOrderStatusChanged(OrderEvent event) {
        System.out.println(
                "[DASHBOARD] Refresh order "
                + event.getOrderId()
                + " with status " + event.getStatus()
        );
    }
}

A dashboard observer might update an in-memory view model or notify another component that the order status needs refreshing. A real browser UI would require an appropriate communication mechanism, such as polling, server-sent events, or WebSockets.

Step 8: Create the main class

public class ObserverPatternDemo {

    public static void main(String[] args) {

        OrderService orderService = new OrderService();

        OrderObserver emailObserver = new EmailObserver();
        OrderObserver smsObserver = new SmsObserver();
        OrderObserver dashboardObserver =
                new DashboardObserver();

        orderService.addObserver(emailObserver);
        orderService.addObserver(smsObserver);
        orderService.addObserver(dashboardObserver);

        orderService.updateStatus(
                "ORD-1001",
                OrderStatus.PLACED
        );

        System.out.println();

        orderService.updateStatus(
                "ORD-1001",
                OrderStatus.SHIPPED
        );
    }
}

The main method connects the subject to three observers. Each time the order service publishes a status change, all three receive the same event and handle it in their own way.

7. Expected Output and Execution Flow

Order ORD-1001 status changed to PLACED
[EMAIL] Order ORD-1001 is now PLACED
[SMS] Order ORD-1001 status: PLACED
[DASHBOARD] Refresh order ORD-1001 with status PLACED

Order ORD-1001 status changed to SHIPPED
[EMAIL] Order ORD-1001 is now SHIPPED
[SMS] Order ORD-1001 status: SHIPPED
[DASHBOARD] Refresh order ORD-1001 with status SHIPPED

The output demonstrates that a single status change can trigger multiple reactions. In this implementation, notifications are synchronous and run sequentially in registration order. If one observer throws an exception, the loop stops and later observers may not receive the event.

What makes this implementation an Observer pattern?

  • The order service is the subject.
  • The email, SMS, and dashboard classes are concrete observers.
  • All observers share the OrderObserver interface.
  • Observers register and can later unregister.
  • The order service does not depend on concrete notification classes.

Adding a new observer, such as an audit handler, does not require adding another concrete notification dependency to the order service. The client only needs to register the new implementation.

8. Registering and Removing Observers

Registration is an important part of the pattern. A component should be able to start listening when it needs updates and stop listening when it no longer needs them.

For example, you can unregister the SMS observer like this:

OrderObserver smsObserver = new SmsObserver();

orderService.addObserver(smsObserver);

// Later, stop receiving order-status events.
orderService.removeObserver(smsObserver);

Keep a reference to the observer you registered if you need to unregister that same instance. Creating a new observer object of the same class does not automatically remove the original instance.

Important considerations

  • A subject retaining observers can keep them reachable longer than expected.
  • Duplicate registration may cause duplicate notifications unless it is prevented.
  • Thread-safe registration requires an appropriate concurrency strategy.
  • Observers should be removed when their subscription is no longer needed.

For a small example, a list is sufficient. For a production event source with concurrent subscriptions, choose a collection and registration model that matches the required thread-safety guarantees.

9. Observer Design Pattern in Spring Boot

Spring Boot offers an application event mechanism that can support observer-style notification. A publisher publishes an event, and registered listeners react to it.

This is useful when several components within the same application context need to respond to an event without the publisher directly calling each listener.

Step 1: Define an order status event

import org.springframework.context.ApplicationEvent;

public class OrderStatusChangedEvent extends ApplicationEvent {

    private final String orderId;
    private final String status;

    public OrderStatusChangedEvent(
            Object source,
            String orderId,
            String status) {
        super(source);
        this.orderId = orderId;
        this.status = status;
    }

    public String getOrderId() {
        return orderId;
    }

    public String getStatus() {
        return status;
    }
}

Step 2: Publish the event

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class SpringOrderService {

    private final ApplicationEventPublisher eventPublisher;

    public SpringOrderService(
            ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void updateStatus(String orderId, String status) {

        // Persist the order status here when appropriate.

        eventPublisher.publishEvent(
                new OrderStatusChangedEvent(
                        this,
                        orderId,
                        status
                )
        );
    }
}

Step 3: Create an email event listener

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class OrderEmailListener {

    @EventListener
    public void handleOrderStatusChanged(
            OrderStatusChangedEvent event) {

        System.out.println(
                "Email update for order "
                + event.getOrderId()
                + ": " + event.getStatus()
        );
    }
}

Step 4: Create an audit event listener

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class OrderAuditListener {

    @EventListener
    public void handleOrderStatusChanged(
            OrderStatusChangedEvent event) {

        System.out.println(
                "Audit event: order "
                + event.getOrderId()
                + " changed to " + event.getStatus()
        );
    }
}

When SpringOrderService publishes the event, Spring discovers the matching listeners and invokes them. The order service does not need direct references to the email or audit listener.

In this example, listeners are synchronous by default. If a listener sends an email and that operation fails, the failure can affect the publisher's call. If asynchronous listeners are configured, their execution and error handling differ and need to be designed explicitly.

Does a Spring event guarantee delivery?

No. A normal Spring application event is an in-process notification. It is not automatically persisted or delivered again after a process crash. For critical notifications, consider a durable messaging architecture, idempotent consumers, retry policies, and an outbox pattern when appropriate.

10. Observer Pattern in Microservices

The same event-driven idea can be applied across service boundaries, but a distributed event architecture introduces concerns that are not solved by a simple in-memory Observer implementation.

Consider a payment application:

  1. A payment service completes a payment and records its status.
  2. A payment-completed event is published.
  3. A notification consumer sends a receipt.
  4. An analytics consumer updates reporting data.
  5. An order consumer updates the order workflow if required.

A broker such as Kafka or RabbitMQ can distribute events to independent consumers. These consumers resemble observers conceptually, but the system is now using distributed messaging rather than the simple direct callback mechanism shown in the plain Java example.

Plain Java Observer vs distributed events

Plain Java Observer Distributed event messaging
Observers usually live in the same process. Producers and consumers may run in separate processes.
Callbacks are often synchronous unless explicitly changed. Message processing is often asynchronous.
Events are not automatically durable. Persistence and delivery behavior depend on broker configuration and architecture.
Exceptions may propagate directly to the caller. Failures require explicit retry, dead-letter, and recovery strategies as appropriate.

For payment and order systems, do not assume that publishing an event automatically makes the database update and message delivery atomic. If losing an event would create an inconsistent business workflow, evaluate the transactional outbox pattern and idempotent consumers.

11. Observer Pattern in Modern Java

Java has several APIs that are related to observer-style notifications. It is important to choose the one that fits the problem.

Custom Observer interface

A custom interface such as OrderObserver is a good option when you want an explicit contract tailored to your application. It works with ordinary Java classes and does not require a framework.

PropertyChangeListener

The java.beans.PropertyChangeListener API can notify listeners when bean properties change. It is useful when the event is specifically a property change and the API's event model matches the use case.

Observable and Observer

Java's legacy java.util.Observable class and java.util.Observer interface were deprecated in Java 9. Avoid introducing them into new applications. Oracle's Java API documentation discusses their limitations and points to alternatives. See the official Observable documentation.

Flow API

The java.util.concurrent.Flow API provides interfaces for reactive-streams-style publishers and subscribers, including demand-aware backpressure. It is more appropriate for certain asynchronous stream-processing use cases than a basic synchronous callback list.

These APIs share some conceptual similarities, but they are not interchangeable. A simple Observer pattern does not automatically provide backpressure, asynchronous processing, or durable delivery.

12. Observer vs Mediator vs Strategy

Observer vs Mediator

Observer Mediator
Notifies registered objects about an event or state change. Coordinates interactions among participating objects.
Focuses on notification and reactions. Focuses on communication rules and coordination.
Example: notify email and SMS handlers about an order status. Example: coordinate interactions among users in a chat room.

Observer vs Strategy

Observer Strategy
Defines a notification relationship between a subject and observers. Defines interchangeable implementations of an algorithm or behavior.
Several observers may react to the same event. A context selects or uses a strategy for a particular task.
Example: several handlers react to a payment-completed event. Example: choose different fee-calculation strategies for payment methods.

13. Advantages and Disadvantages

Advantages

  • Loose coupling: The subject does not need to depend on every concrete observer.
  • Extensibility: New observers can be registered without adding another concrete observer dependency to the subject.
  • Separation of concerns: Each observer handles its own reaction to an event.
  • Reusability: Observers can often be reused with different subjects that expose a compatible event contract.
  • Testability: The subject and individual observers can be tested separately.

Disadvantages

  • Hidden execution flow: It may be harder to follow every action triggered by one event.
  • Failure handling: One failing observer may prevent other observers from receiving a notification if exceptions are not handled.
  • Lifecycle management: Forgotten registrations can retain objects longer than expected.
  • Notification overhead: Publishing an event to many observers requires additional work.
  • Ordering concerns: Some applications need a defined execution order, which should be specified rather than assumed.

14. Best Practices for Java Developers

  1. Use a clear observer interface. Prefer a typed method such as onOrderStatusChanged(OrderEvent event) over a generic method that accepts an unstructured object.
  2. Pass only the necessary data. Event objects should expose the information observers need without forcing them to depend on the entire subject.
  3. Keep events immutable where practical. This reduces the chance that one observer changes data another observer expects to receive.
  4. Define exception handling. Decide whether observer failures should stop notification, be logged and isolated, or trigger a retry mechanism.
  5. Manage subscriptions carefully. Provide an unsubscribe mechanism when observers have a limited lifecycle.
  6. Make concurrency assumptions explicit. A normal ArrayList observer registry is not safe for arbitrary concurrent registration and notification.
  7. Do not assume asynchronous execution. Plain Java callbacks and Spring application events are commonly synchronous unless asynchronous behavior is configured.
  8. Use durable messaging for critical cross-service events. Payment and order workflows may need persistence, retries, idempotency, and recovery beyond the basic Observer pattern.
  9. Avoid unnecessary abstraction. If only one component needs to be called and the relationship is simple, a direct method call may be easier to maintain.

15. Java Interview Questions and Answers

Q1. What is the Observer Design Pattern?

It is a behavioral design pattern that defines a one-to-many relationship. When a subject changes state or publishes an event, registered observers are notified.

Q2. What are the main participants in the Observer pattern?

The main participants are the subject, observer interface, concrete observers, and the client that connects them.

Q3. Give a real-world example of Observer.

An e-commerce order notification system is a practical example. Email, SMS, and dashboard handlers can all react when the order status changes.

Q4. How does Observer reduce coupling?

The subject communicates through a common observer contract instead of calling each concrete notification implementation directly.

Q5. What is the difference between Observer and Publisher-Subscriber?

They share a notification idea, but the classic Observer pattern usually has a subject maintaining direct references to its observers. A publisher-subscriber system often introduces a broker or event channel between publishers and subscribers. The terms are related, but the architectures are not identical.

Q6. Is the Observer pattern synchronous or asynchronous?

It can be implemented either way. The simple Java callback implementation in this article is synchronous. Asynchronous execution requires a suitable executor, messaging mechanism, or event framework.

Q7. What happens if one observer throws an exception?

It depends on the subject's notification logic. In a simple loop without exception handling, the exception may stop the loop and prevent later observers from being notified.

Q8. How can an observer unregister?

The subject can expose a removal method such as removeObserver(). The client should retain the registered instance or a subscription handle so it can unsubscribe.

Q9. Is Java's Observable class recommended in new applications?

No. java.util.Observable and java.util.Observer have been deprecated since Java 9. A custom interface or a more suitable modern API is generally a better choice.

Q10. Can Spring Boot implement Observer-style notifications?

Yes. Spring application events and @EventListener methods allow application components to publish and react to events. Normal in-process events are not a durable messaging system.

Q11. How does Observer differ from Mediator?

Observer primarily notifies interested subscribers about an event or state change. Mediator coordinates how a group of objects interact with one another.

Q12. What is a possible memory issue with Observer?

A long-lived subject may keep strong references to observers that are no longer needed. Removing subscriptions when they are no longer required helps avoid unnecessary object retention.

Q13. Does Observer guarantee that every notification is delivered?

No. A basic in-memory implementation provides no durable delivery guarantee. If delivery must survive process failures, use a suitable persistent messaging or event-processing architecture.

Q14. When should you avoid the Observer pattern?

Avoid it when the dependency is simple and stable, when notifications make control flow unnecessarily difficult to follow, or when the application requires stronger delivery guarantees than the implementation provides.

Q15. How would you unit-test an Observer implementation?

Test that the subject notifies registered observers, does not notify removed observers, sends the correct event data, and behaves according to the chosen exception and ordering rules.

16. Frequently Asked Questions

Is Observer a behavioral design pattern?

Yes. It describes a communication relationship in which a subject notifies interested observers about changes or events.

Can one subject have multiple observers?

Yes. A subject can notify any number of registered observers, subject to the limits and rules of its implementation.

Can one observer listen to multiple subjects?

Yes. An observer can be registered with multiple subjects if its implementation and event-handling logic support that arrangement.

Does Observer automatically make an application thread-safe?

No. Thread safety depends on how observers are registered, how notifications are delivered, and whether the observed state is accessed concurrently.

Can Observer be used for payment notifications?

Yes, conceptually. However, payment workflows often require durable event publication, idempotent consumers, and reliable recovery mechanisms. A simple in-memory observer list does not provide those guarantees.

What is the difference between Observer and Spring events?

A custom Observer implementation typically manages its own observer list and invokes callbacks directly. Spring application events provide a framework-managed publication and listener mechanism. Both can support observer-style notification, but their lifecycle, threading, and error-handling behavior differ.

17. Quick Revision Checklist

  • Observer is a behavioral design pattern.
  • It defines a one-to-many notification relationship.
  • The subject manages registered observers.
  • Concrete observers implement a common interface.
  • An event object can carry the details of a state change.
  • Observers should be registered and removed appropriately.
  • Basic callbacks are not automatically asynchronous or thread-safe.
  • Java's legacy Observable and Observer APIs are deprecated.
  • Spring application events can support in-process event notification.
  • Critical distributed workflows may require durable messaging and recovery mechanisms.

Conclusion

The Observer Design Pattern is useful when one object needs to notify several interested components without depending directly on every component's implementation.

Our e-commerce example demonstrates this clearly. When an order status changes, email, SMS, and dashboard observers receive the event and perform their own tasks. The order service remains focused on publishing the status change instead of managing every notification channel itself.

In Spring Boot, application events can provide a convenient framework-managed approach for similar in-process notifications. For distributed systems, however, reliability, ordering, retries, and persistence must be considered separately.

The most important lesson is not simply how to implement an observer interface. It is knowing when event-based notification makes a design easier to extend and when a direct method call would be simpler.

Continue learning Java design patterns such as Mediator, Strategy, Factory, and Chain of Responsibility to understand how different patterns solve different design problems.

No comments: