Skip to main content
Create your own
Lesson illustration

Implementing Event-Driven Communication with the Observer Pattern

Hello! Welcome to your first lesson in the "Behavioral Design Patterns" module.

In the previous module, we explored structural patterns, which focus on how objects are composed into larger structures. Now, we shift our focus to behavioral patterns, which are all about how objects communicate and delegate responsibilities.

Today's lesson covers one of the most fundamental and widely used behavioral patterns: the Observer pattern. Your learning outcome is to apply the Observer pattern to implement event-driven communication between objects.

This pattern is the cornerstone of event-driven architecture, a style you've likely encountered in frameworks like Spring Boot, for instance, with its ApplicationEvent and @EventListener mechanisms. Mastering the Observer pattern is crucial for designing systems where components need to react to changes without being tightly coupled, a common requirement in system design interviews.

The Problem: The Tightly Coupled Monolith

Imagine you're building a system where a central component's state can change, and several other, unrelated components need to be notified to perform their own tasks. For example, when a user's profile is updated, you might need to:

  • Update the UI.
  • Invalidate a cache.
  • Send a notification email.
  • Log the change for auditing.

A naive approach would be to have the UserProfile object directly call methods on the UIUpdater, CacheManager, EmailService, and AuditLogger. What's wrong with this?

To see the issues this creates, let's examine a concrete example of a Fitness Tracker app.

Observer | LLD

This article from Algomaster, 'Observer | LLD', uses a Fitness Tracker app to demonstrate a naive, tightly coupled design. It clearly lays out the problems that arise from this approach.

Please read section '1. The Problem: Broadcasting Fitness Data'. Pay close attention to the 'Problems with This Approach' subsection. This highlights why a direct-call approach doesn't scale well and violates key design principles.

As the article points out, the naive approach leads to:

  • Tight Coupling: The FitnessData object (the subject) is directly tied to every observer.
  • Violation of the Open/Closed Principle: Adding a new observer (e.g., a SocialSharingService) requires modifying the FitnessData class.
  • Responsibility Bloat: The subject becomes responsible not just for its own state but also for coordinating all its dependents, violating the Single Responsibility Principle.

We need a way to flip this dependency. Instead of the subject knowing about its observers, the observers should simply register their interest in the subject.

The Solution: Introducing the Observer Pattern

The Observer pattern provides an elegant solution by defining a one-to-many dependency between objects. When one object, the Subject (also called the Observable or Publisher), changes state, all its dependents, the Observers (or Subscribers), are notified and updated automatically.

This video provides an excellent introduction using a simple store-customer analogy and then builds the Java implementation step-by-step.

The Observer Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific

This video from the Geekific channel, 'The Observer Pattern Explained and Implemented in Java', provides a clear conceptual overview and a simple implementation.

Watch from the beginning up to 03:48. This covers: The core problem the pattern solves (0:00). The basic Java implementation with a publisher and subscriber (1:24). How easily the design can be extended with new subscriber types without changing the publisher's code (2:40).

Core Components

The pattern typically consists of four main components, which can be visualized in a class diagram.

UML Class Diagram of Observer Design Pattern (Weather Station Example)
This UML diagram shows the classic Weather Station example. `WeatherStation` is the Concrete Subject that notifies its observers (`PhoneDisplay`, `TVDisplay`) when its data changes.
  1. Subject Interface: Declares methods for managing observers, typically registerObserver(), removeObserver(), and notifyObservers().
  2. Observer Interface: Declares the update() method, which the subject calls to notify the observer of a change.
  3. Concrete Subject: Implements the Subject interface. It maintains a list of observers and contains the state that observers are interested in. When its state changes, it calls notifyObservers().
  4. Concrete Observer: Implements the Observer interface. Each concrete observer registers with a concrete subject and implements the update() method to define its reaction to the notification.

Now, let's see how these components are used to refactor the Fitness Tracker app we discussed earlier.

Observer | LLD

Returning to the Algomaster article, let's see how it applies the Observer pattern to fix the initial design.

Read sections '2. Understanding the Observer Pattern', '3. Implementing Observer Pattern', and 'What We Achieved'. Compare the provided Java code with the UML diagram above. Notice how the FitnessData class no longer knows about specific observers like LiveActivityDisplay, only about the FitnessDataObserver interface. This is the essence of decoupling.

The refactored design achieves:

  • Loose Coupling: The subject only knows it has a list of objects that implement the Observer interface. It doesn't know or care about their concrete types.
  • Extensibility: You can add new observers at any time without a single change to the subject's code.

Implementing the Observer Pattern in Java

While implementing the pattern with your own interfaces is excellent for understanding, modern Java offers built-in utilities. Given your experience, it's important to know the standard and recommended approaches.

The Observer Pattern in Java

The Baeldung article 'The Observer Pattern in Java' covers two built-in Java approaches. We'll look at the deprecated one for context, and then focus on the modern, recommended one.

Please read sections 3 and 4 of the article: Section 3 ('Implementation With Observer'): Understand how the java.util.Observer interface and Observable class worked. Note the reasons it has been deprecated since Java 9 (primarily because Observable is a class, which restricts its use in inheritance hierarchies). Section 4 ('Implementation With PropertyChangeListener'): This is the key part. Focus on how the java.beans.PropertyChangeSupport class is used to manage listeners and fire events. This is the standard, modern way to implement the Observer pattern for property changes in Java beans.

The PropertyChangeListener approach is powerful because it provides a standardized way for objects (often UI components or data models) to react to property changes, and the PropertyChangeSupport helper class handles all the boilerplate logic for managing listeners.

Test your understanding!

You are designing an e-commerce platform. The InventoryService manages the stock level of products. You need to implement the following features:

  • When a product's stock drops below a certain threshold (e.g., 10 items), a RestockService should be notified to automatically create a purchase order.
  • When a product goes out of stock, a NotificationService should be notified to email all users who have wishlisted that product.

How would you use the Observer pattern to design this? Identify the Subject, Observers, and the trigger for notification.

Show answer
  • Subject: The Product class (or a specific ProductInventory class managed by InventoryService). It holds the state (stock level).
  • Concrete Observers:
    1. RestockService: Implements the Observer interface. Its update() method checks if the stock is below the threshold and creates a purchase order.
    2. NotificationService: Implements the Observer interface. Its update() method checks if the stock is zero and sends emails to wishlisters.
  • Notification Trigger: The updateStock() method (or a similar method) within the Product/ProductInventory class. After updating the stock count, it would call notifyObservers(). The observers then inspect the product's new state to decide whether to act.

Advanced Topic: Push vs. Pull and Memory Leaks

Two important considerations come up when implementing the Observer pattern in real-world applications.

1. Push vs. Pull Model

How does the observer get the updated state from the subject?

  • Push Model: The subject "pushes" all relevant data to the observer as arguments in the update() method (e.g., update(newState, oldState)). This is simple but can be inefficient if observers don't need all the data.
  • Pull Model: The subject passes a reference to itself in the update() method (e.g., update(subject)). The observer then "pulls" only the data it needs from the subject using getter methods. This is more flexible but may result in more method calls.

This video segment explains the trade-offs clearly.

Observer Design Pattern | Best Video to Understand | Low Level Design | Java | OOPS

This clip from a video by Riddhi Dutta explains the important design choice between the 'push' and 'pull' models of data transfer from subject to observer.

Watch from 12:45 to 15:10. Pay attention to the difference between pushing all data in the update method versus allowing observers to pull data via getters and the implications for the design.

2. The Lapsed Listener Problem (Memory Leaks)

A common pitfall is when an observer is no longer needed but is not explicitly removed from the subject's list of observers. The subject holds a strong reference to the observer, preventing it from being garbage collected. This is a memory leak.

To avoid this, you must ensure that observers are always deregistered when they are disposed of. A more advanced solution involves using java.lang.ref.WeakReference in the subject's list, which allows the garbage collector to reclaim observers even if they haven't been explicitly deregistered.

Conclusion

In this lesson, you learned how to apply the Observer pattern to build flexible, event-driven systems. This is a foundational skill for decoupling components in low-level design.

Key Takeaways:

  • The Observer pattern defines a one-to-many dependency where a Subject notifies multiple Observers of state changes.
  • It promotes loose coupling, allowing you to add or remove observers without modifying the subject, thus adhering to the Open/Closed Principle.
  • The core components are the Subject and Observer interfaces and their concrete implementations.
  • In modern Java, it's best to use the java.beans.PropertyChangeListener and PropertyChangeSupport classes for property-based observation.
  • When implementing, consider the trade-offs of the push vs. pull model and be mindful of the lapsed listener problem to prevent memory leaks.

In our next lesson, we will explore the Chain of Responsibility pattern. While the Observer pattern broadcasts a change to all interested parties, the Chain of Responsibility pattern creates a chain of objects to process a request, decoupling the sender from the ultimate receiver in a very different way.

Can't find a good explanation? Sign up and we'll make it for you

Sign up