Unveiling the Observer Pattern: From UI Buttons to Spring Events

Andy YangAndy Yang
5 min read

Design patterns are the foundation of software development, providing best practices for solving common problems. Today, we’ll dive into a simple yet powerful behavioral pattern: the Observer Pattern.

If you’ve ever used a frontend framework or written any program with a UI, you’ve probably applied this pattern without even realizing it.


1. What is the Observer Pattern?

Imagine you subscribe to a newspaper. Once the publisher releases a new issue (state change), they deliver it to your home automatically. You don’t need to call every day asking, “Is there a new paper today?”

This is the essence of the Observer Pattern: Define a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.

The pattern achieves loose coupling. The subject only handles notifications—it doesn’t need to know who is listening or what they will do with the notification. This makes the system more flexible and easier to extend.


2. The Three Core Roles of the Observer Pattern

To implement this pattern, we need three key components:

  • Observer: Defines an interface that all “subscribers” must implement. Typically, this interface has a single method (e.g., update()) that receives notifications from the subject.

  • Subject: Manages a list of observers. It provides methods to subscribe (attach()), unsubscribe (detach()), and notify (notify()). When its state changes, it calls notify() to update all registered observers.

  • Concrete Implementations: Specific classes that implement Observer and Subject. For example, a Button class can act as a concrete subject, while a TextDisplay can be a concrete observer.


3. UML Class Diagram

Here’s a visual look at the relationship between these roles:

Notice that ConcreteSubject only depends on the Observer interface, not on specific observer classes. This demonstrates programming to an interface, which effectively reduces coupling.


4. Java Implementation

Let’s implement this pattern in Java to see how the components work together.

1. Observer Interface

interface Observer {
    void update(String message);
}

2. Subject Interface

interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers(String message);
}

3. Concrete Observer A

class ConcreteObserverA implements Observer {
    private String name;

    public ConcreteObserverA(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received notification: " + message);
        // Example: update UI
    }
}

4. Concrete Observer B

class ConcreteObserverB implements Observer {
    private String name;

    public ConcreteObserverB(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received notification: " + message);
        // Example: log event
    }
}

5. Concrete Subject

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

class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String subjectState;

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
        System.out.println("Subscribed: " + ((ConcreteObserverA) observer).name);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
        System.out.println("Unsubscribed: " + ((ConcreteObserverA) observer).name);
    }

    @Override
    public void notifyObservers(String message) {
        System.out.println("Subject state changed, notifying observers...");
        for (Observer observer : observers) {
            observer.update(message);
        }
    }

    public void changeState(String newState) {
        this.subjectState = newState;
        System.out.println("Subject state changed to: " + newState);
        notifyObservers("State updated to: " + newState);
    }
}

6. Main Program

public class ObserverPatternDemo {
    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();

        ConcreteObserverA observer1 = new ConcreteObserverA("Observer A");
        ConcreteObserverB observer2 = new ConcreteObserverB("Observer B");

        subject.attach(observer1);
        subject.attach(observer2);

        System.out.println("--------------------");

        subject.changeState("Order Created");

        System.out.println("--------------------");

        subject.detach(observer1);

        System.out.println("--------------------");

        subject.changeState("Order Shipped");
    }
}

5. From Theory to Practice: UI Events and Business Logic

Now let’s see how this pattern applies in real development, from UI events to backend workflows.

UI Event Listening: Decoupling Buttons from Business Logic

Consider a Register button. Traditionally, its click handler might directly call methods like UserService.register() and NotificationService.sendWelcomeEmail(). This tightly couples UI and business logic.

With the Observer Pattern:

  1. Subject (Button): The button maintains a list of observers and provides attach() / detach().

  2. Observer (RegisterController): Implements Observer and defines its response in update().

  3. Subscription: During initialization, button.attach(registerController).

  4. Notification: On click, button.notifyObservers().

  5. Response: RegisterController.update() executes business logic.

Thus, the Button only knows it was clicked—it doesn’t care what happens next. UI and business logic are decoupled.

Business Events: Order and Notifications

In e-commerce, when an order is placed, multiple things may happen: send confirmation email, add reward points, log the action. If all of this is coded in one method, adding new features (e.g., SMS notification) requires modifying existing code.

With events:

  1. Define an event class OrderCreatedEvent.

  2. Publish the event when an order is created.

  3. Independent services (Email, Points, Logging) subscribe to this event and handle their own logic.

This allows new functionality to be added without touching existing order logic, demonstrating the Open/Closed Principle.


6. Real-World Example: Spring Events

Modern frameworks, like Spring, integrate the Observer Pattern via their event system.

  • Subject: ApplicationContext

  • Observer: Components that implement ApplicationListener or use @EventListener

Example:

Event Definition

import org.springframework.context.ApplicationEvent;

public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;

    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

Publishing the Event

@Autowired
private ApplicationEventPublisher eventPublisher;

public void createOrder(Order order) {
    // Order business logic...
    eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
}

Listening to the Event

@Component
public class EmailService {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Send email logic...
    }
}

Here, the order service doesn’t know who’s listening. Adding SMS notifications or inventory updates is as simple as writing another listener—no changes to existing code.


7. Conclusion

The Observer Pattern is an elegant and powerful tool for decoupling object dependencies, making code more flexible, maintainable, and extensible. From simple UI events to distributed message queues, its principles are everywhere. Mastering it equips you to write code that’s easier to evolve and scale.

0
Subscribe to my newsletter

Read articles from Andy Yang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andy Yang
Andy Yang