Understanding the Observer Design Pattern (With a Simple Book Club Java Example)

File:Observer w update.svg

The Observer pattern is a behavioral design pattern that defines a one-to-many relationship between objects. When the state of one object (the subject) changes, all its dependents (the observers) are automatically notified and updated. This pattern promotes loose coupling between the subject and its observers and is widely used in event-driven systems.

What do you need to implement it on a high level?

  • Define two interfaces: Observer and Subject

  • Define a concrete implementation of the Subject

  • Define as many concrete implementations of the Observer interface as you need

  • Wired everything together externally


✨ Key Characteristics of the Observer Pattern

  • One-to-many dependency: One subject notifies multiple observers.

  • Loose coupling: Subjects don't need to know the concrete details of their observers.

  • Dynamic subscription: Observers can be added or removed at runtime.

  • Push or pull model: The subject may either send specific data or let the observer pull the data it needs.


🧠 When Should You Use It?

Use the Observer pattern when:

  • You have a data source or subject whose state changes over time.

  • Multiple parts of your system need to respond to that change.

  • You want to decouple the logic of the source from its listeners.

It is common in:

  • UI frameworks (e.g., event listeners in Java Swing or JavaScript)

  • MVC architectures

  • Logging systems

  • Notification and alerting systems


📚 A Concrete Example: Book Club Announcement System

Let’s implement the Observer pattern using a simple domain: a Book Club. The book club announces new books on different schedules (monthly, biweekly, yearly), and its members are notified via different channels (email, push notification).

We’ll begin by defining the core interfaces for the pattern. This will help establish the contract for communication between the subject and its observers.

🔌 Step 1: Define Observer and Subject Interfaces

The Observer interface represents any component that wants to receive updates. The Subject interface is implemented by the class that owns the state and sends updates.

public interface Observer {
  void update(ScheduleType scheduleType, String bookTitle);
}

public interface Subject {
  void addObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers(ScheduleType scheduleType, String bookTitle);
}

📦 Step 2: Create the Concrete Subject - BookClub

The BookClub class implements the Subject interface. It stores a list of observers and notifies them whenever a new book is announced. The announcement can happen on different schedules — monthly, biweekly, or yearly — using clearly named methods like announceMonthlyBook. This improves clarity and reusability, as each method encapsulates a specific context in which observers are notified.

This implementation also demonstrates a push model: the subject actively pushes data (schedule type and book title) to all observers when a change occurs.

🧠 In contrast, a pull model would mean the subject simply notifies observers that something changed, and the observers then call back to the subject to fetch the updated data. For that, the update() method in the Observer interface might receive no arguments, and observers would query the BookClub to retrieve the latest book title themselves.

public class BookClub implements Subject {
  public enum ScheduleType {
    BIWEEKLY, MONTHLY, YEARLY
  }

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

  @Override
  public void addObserver(Observer observer) {
    observers.add(observer);
  }

  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }

  @Override
  public void notifyObservers(ScheduleType scheduleType, String bookTitle) {
    System.out.printf("Announcing new book, scheduleType: %s, bookTitle: %s%n", scheduleType, bookTitle);
    observers.forEach(o -> o.update(scheduleType, bookTitle));
    System.out.println("------------");
  }

  public void announceMonthlyBook(String bookTitle) {
    notifyObservers(ScheduleType.MONTHLY, bookTitle);
  }

  public void announceBiweeklyBook(String bookTitle) {
    notifyObservers(ScheduleType.BIWEEKLY, bookTitle);
  }

  public void announceYearlyBook(String bookTitle) {
    notifyObservers(ScheduleType.YEARLY, bookTitle);
  }
}

📬 Step 3: Create the Concrete Observers

Observers react to announcements in different ways. Below are two examples: one sends an email and another sends a push notification.

public class EmailNotificationMember implements Observer {
  @Override
  public void update(ScheduleType scheduleType, String bookTitle) {
    System.out.printf("[Email] New %s book: '%s'%n", scheduleType, bookTitle);
  }
}

public class PushNotificationMember implements Observer {
  @Override
  public void update(ScheduleType scheduleType, String bookTitle) {
    System.out.printf("[PushNotification] New %s book: '%s'%n", scheduleType, bookTitle);
  }
}

Each observer implements the update method, which is called by the BookClub subject.

🚀 Step 4: Wiring it Together in the Main App

The main application is where we create the BookClub, register observers, and simulate announcements.

public class BookClubApp {
  public static void main(String[] args) {
    BookClub nonFictionClub = new BookClub();
    nonFictionClub.addObserver(new EmailNotificationMember());
    nonFictionClub.addObserver(new PushNotificationMember());

    nonFictionClub.announceMonthlyBook("Atomic Habits");
    nonFictionClub.announceBiweeklyBook("Hellen Keller Summarized Bio");
    nonFictionClub.announceYearlyBook("A History of Ancient Rome");
  }
}

When run, this will output messages showing which observers received which book announcements.


🔍 Beyond the Basics: Real-World Design Considerations

📤 Push vs Pull Model

Our BookClub example uses a push model: the subject passes all necessary data directly to the observers. This works well for small examples but may lead to long parameter lists and tight coupling as the data grows.

💡 In such cases, it's better to encapsulate the data being sent in a dedicated class (e.g., BookAnnouncement) to keep the observer interface clean and extendable.

public class BookAnnouncement {
  private ScheduleType scheduleType;
  private String bookTitle;
  // getters, constructors, etc.
}

Then the observer interface could change to:

void update(BookAnnouncement announcement);

🧠 In contrast, a pull model would mean the subject simply notifies observers that something changed, and the observers then call back to the subject to fetch the updated data. For that, the update() method in the Observer interface might receive no arguments, and observers would query the BookClub to retrieve the latest book title themselves.

🧩 Modeling Real Members vs Notification Channels

Our example keeps things simple by treating notification channels (email, push) as observers. But in a real system, you'd model things differently.

🧍 Option 1: Member as the Observer

public class Member implements Observer {
  private String name;
  private String email;
  private boolean wantsMonthlyOnly;

  @Override
  public void update(ScheduleType scheduleType, String bookTitle) {
    if (wantsMonthlyOnly && scheduleType != ScheduleType.MONTHLY) return;
    System.out.printf("Email to %s: New %s book: '%s'%n", name, scheduleType, bookTitle);
  }
}

📡 Option 2: Notification Observers Acting on Member

public class EmailNotifier implements Observer {
  private List<Member> members;

  @Override
  public void update(ScheduleType scheduleType, String bookTitle) {
    for (Member m : members) {
      if (!m.prefersEmail()) continue;
      System.out.printf("[Email] To %s: '%s' (%s)%n", m.getEmail(), bookTitle, scheduleType);
    }
  }
}

This second approach decouples domain from infrastructure, scales better, and aligns more closely with real-world design needs.


🧩 Observer Pattern in Frameworks and Real Systems

In real-world software systems, the Observer pattern appears frequently — even if you don’t always implement it directly. Recognizing it helps you understand the underlying architecture of many tools and frameworks.

Some common real-world manifestations include:

  • Pub/Sub architectures: Systems like Apache Kafka, RabbitMQ, or Google Pub/Sub implement the Observer pattern at scale. Publishers emit events to a channel or topic, and subscribers react to those events.

  • UI frameworks: In Java Swing, React, or Android, you often attach listeners or observers to UI components to handle events.

  • JavaScript: Methods like addEventListener in the DOM model exemplify the pattern.

  • Spring Framework: The @EventListener annotation enables event-driven programming by observing and reacting to domain events.

  • Reactive Programming: Libraries like RxJava, Project Reactor, or RxJS are built entirely around observables and subscribers.

Understanding the pattern beneath these tools gives you an edge when debugging, designing for extensibility, or choosing the right communication strategy in your system.


✅ Conclusion

The Observer pattern is powerful when you need dynamic, loosely-coupled communication between components. While you may not implement it often by hand, understanding it helps you better navigate modern event-driven and reactive systems.

The BookClub example demonstrates the essentials, and the design reflection helps bridge the gap between learning and applying the pattern effectively in real-world systems.

Have you ever implemented the Observer pattern yourself, or mostly seen it abstracted in frameworks? I’d genuinely love to hear your experience. Whether you’re revisiting the pattern or discovering it for the first time, I hope this article gave you a clearer picture of how it works — and why it matters. Thanks for reading, and happy coding!


0
Subscribe to my newsletter

Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero

I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.