Unveiling the Observer Pattern: From UI Buttons to Spring Events


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 callsnotify()
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 aTextDisplay
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:
Subject (
Button
): The button maintains a list of observers and providesattach()
/detach()
.Observer (
RegisterController
): ImplementsObserver
and defines its response inupdate()
.Subscription: During initialization,
button.attach(registerController)
.Notification: On click,
button.notifyObservers()
.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:
Define an event class
OrderCreatedEvent
.Publish the event when an order is created.
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.
Subscribe to my newsletter
Read articles from Andy Yang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
