Observer Design Pattern in Java

The Observer pattern lets a publisher notify registered listeners. Follow a Java example where one listener unsubscribes during a notification and see who receives the next message.

Observer Pattern

A publisher often needs to tell several parts of an application that something happened. The Observer pattern lets those parts register as listeners. When the publisher has an update, it calls every registered listener through one shared interface. The publisher can notify listeners without knowing their individual classes.

“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”

We will use a small message feature: one publisher announces a message, while two listeners print what they receive. One listener removes itself during the first notification. That gives us a useful way to see which listeners receive the next message.

In this example, publishing a new message is the update that interests listeners. The publisher does not need to keep a lasting copy of each message for the notification relationship to work.

1. How the Observer Pattern Works

The publisher is sometimes called the subject. An observer is any object that subscribes to its updates. Subscribing adds an observer to the publisher’s list; unsubscribing removes it. The publisher calls each observer through the same notification method.

Participant Role
Subject Keeps the active subscriptions and announces an update.
Observer interface Declares the notification method.
Concrete observers Respond to each notification in their own way.
Client Connects observers to the subject.

The subject does not need a branch for every observer type. It only needs to know that every registered observer supports the interface. The generic diagram shows one implementation of that contract; the subject can hold several observer instances.

Generic Observer UML diagram with a subject holding subscriptions and notifying an observer implementation

Observer describes who gets notified, not how delivery is scheduled. The example below calls listeners in the current thread. A queue or broker would add separate delivery behavior.

2. Observer Pattern in Action

In our message feature, MessagePublisher announces a Message. A MessageListener receives it through update(). Alex’s listener removes itself after its first update, while Brian’s listener remains subscribed. The publisher keeps the listener set, so neither listener needs a reference to the other.

Pattern role Example class or interface Job in the message feature
Subject MessagePublisher Registers listeners and delivers each message.
Observer interface MessageListener Declares update(Message).
Concrete observers OneTimeListener and Brian’s lambda Print a message; the first also unsubscribes itself.
Client ObserverDemo Creates listeners, subscribes them and publishes messages.

Alex and Brian are subscribed when the first message starts. The publisher takes a snapshot, a fixed list of the listeners registered at that moment, and calls both in registration order. Alex’s removal changes the publisher’s set, so the list for the second message contains only Brian. In the class diagram, MessagePublisher knows the listener interface; OneTimeListener is one implementation of it.

Observer example UML diagram showing ObserverDemo, MessagePublisher, MessageListener and OneTimeListener

3. Observer Pattern Implementation

The complete runnable source on GitHub includes the package declaration and imports omitted from these excerpts. It uses one file so we can compile and run the example with two commands.

3.1. Define the Message and Listener

Message is a Java record, a compact class for carrying data. Its text goes to each listener, and its constructor rejects null text. MessageListener declares the operation that the publisher calls, so listener implementations can decide for themselves what to do with the message.

record Message(String text) {
    Message {
        Objects.requireNonNull(text, "text");
    }
}

@FunctionalInterface
interface MessageListener {
    void update(Message message);
}

The text is a String, so one listener cannot change the text that another listener reads. A record containing a mutable object would not automatically make that object immutable.

3.2. Keep the Active Subscriptions

MessagePublisher stores listeners in a LinkedHashSet. If two listener objects compare equal, the set treats them as one registration. It also preserves registration order. Before calling update(), publish() copies the set with List.copyOf(). That list is the recipient set for this one message; changes to the publisher’s set affect later messages.

final class MessagePublisher {
    private final Set<MessageListener> listeners = new LinkedHashSet<>();

    void subscribe(MessageListener listener) {
        listeners.add(Objects.requireNonNull(listener, "listener"));
    }

    void unsubscribe(MessageListener listener) {
        listeners.remove(listener);
    }

    void publish(Message message) {
        Objects.requireNonNull(message, "message");
        for (MessageListener listener : List.copyOf(listeners)) {
            listener.update(message);
        }
    }
}

The copied list prevents a listener from changing the collection being iterated during its own callback. It does not make the publisher safe for concurrent use. If several threads can modify registrations while another thread publishes, the application needs a separate synchronization policy.

3.3. Let One Listener Leave

OneTimeListener holds the publisher so it can remove itself after it receives a message. Its update() method prints the text first, then calls unsubscribe(this). Brian uses a shorter lambda because he stays subscribed throughout the demo.

final class OneTimeListener implements MessageListener {
    private final MessagePublisher publisher;
    private final String name;

    OneTimeListener(MessagePublisher publisher, String name) {
        this.publisher = Objects.requireNonNull(publisher, "publisher");
        this.name = Objects.requireNonNull(name, "name");
    }

    @Override
    public void update(Message message) {
        System.out.println(name + " received: " + message.text());
        publisher.unsubscribe(this);
    }
}

4. Demo

The demo subscribes Alex before Brian and publishes two messages. Alex removes himself during the first callback. The publisher still calls Brian from that first snapshot, then starts a new snapshot for the second message.

public class ObserverDemo {
    public static void main(String[] args) {
        MessagePublisher publisher = new MessagePublisher();
        MessageListener alex = new OneTimeListener(publisher, "Alex");
        MessageListener brian = message ->
                System.out.println("Brian received: " + message.text());

        publisher.subscribe(alex);
        publisher.subscribe(brian);
        publisher.publish(new Message("First message"));
        publisher.publish(new Message("Second message"));
    }
}

From the extracted source folder, run:

javac -d target/observer-demo src/main/java/com/howtodoinjava/patterns/behavioral/observer/ObserverDemo.java
java -cp target/observer-demo com.howtodoinjava.patterns.behavioral.observer.ObserverDemo
Alex received: First message
Brian received: First message
Brian received: Second message

Alex appears only for the first message. Brian receives both. Unsubscribing during one callback changes later notifications, while the current snapshot stays fixed. The order shown here comes from this example’s LinkedHashSet, not from a universal Observer rule.

5. Similar Patterns

Design How communication works Choose it when
Observer A subject calls every registered listener for an update. One event needs to reach independent subscribers.
Mediator A mediator coordinates interactions and may choose a recipient. Several objects share interaction rules that need one home.
Direct callback One object calls a known recipient. A fixed pair of objects needs a simple notification.

A listener exception in this synchronous implementation would interrupt publish() before later listeners are called. An application that needs delivery guarantees, retries or isolation must define those policies explicitly; the Observer pattern does not supply them by itself.

6. FAQs

6.1. What Happens If I Subscribe the Same Listener Twice?

This implementation uses a Set, so two registrations that compare equal occupy one place. A list-based publisher could allow duplicates. The pattern does not require a particular collection; choose behavior that fits the subscription contract.

6.2. Can a Listener Unsubscribe During a Notification?

Yes, in this example. publish() first copies the registered listeners, then iterates over that copy. Alex can remove himself from the publisher’s set without changing the current list. The next call to publish() sees the updated set.

6.3. Are Notifications Ordered or Concurrent?

Our LinkedHashSet delivers in registration order, and the callbacks run one after another on the caller’s thread. Another implementation may choose different ordering or scheduling. This publisher is not designed for a registration change while another thread is preparing a notification.

7. Conclusion

Observer lets a publisher notify interested objects through one interface. The message example makes the subscription lifecycle visible: both listeners receive the first message, and only Brian receives the second after Alex leaves. Use it when several independent responses belong to the same update; keep the delivery rules explicit for your application.

Happy Learning!

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.