🧩 Section 2 · Question #11

How is Observer implemented in Java

The Observer pattern (also known as Publish-Subscribe) is a Behavioral design pattern that establishes a one-to-many subscription mechanism: when an object (the Subject or Publi...


🟢 Junior Level

The Observer pattern (also known as Publish-Subscribe) is a Behavioral design pattern that establishes a one-to-many subscription mechanism: when an object (the Subject or Publisher) undergoes a state change or produces an event, all registered dependents (Observers or Subscribers) are notified and updated automatically.


Real-World Analogy

Subscribing to a newsletter or YouTube channel: The content creator (Publisher) does not need to know individual subscribers personally. Viewers subscribe, and whenever a new video is published, an automatic notification is broadcast to all active subscribers (Observers) without subscribers having to poll the channel repeatedly.


Pure Java Implementation

// 1. Observer Interface
@FunctionalInterface
public interface EventListener {
    void onEvent(String message);
}

// 2. Publisher (Subject)
public class EventPublisher {
    private final List<EventListener> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(EventListener listener) {
        listeners.add(listener);
    }

    public void unsubscribe(EventListener listener) {
        listeners.remove(listener);
    }

    public void notifyListeners(String data) {
        for (EventListener listener : listeners) {
            listener.onEvent(data);
        }
    }
}

// 3. Client Usage (with Java 8+ Lambdas)
EventPublisher publisher = new EventPublisher();

publisher.subscribe(msg -> System.out.println("Email Service: " + msg));
publisher.subscribe(msg -> System.out.println("SMS Gateway: " + msg));

publisher.notifyListeners("Order #1042 successfully placed.");

🟡 Middle Level

Why java.util.Observer and Observable Were Deprecated in Java 9

Java originally provided built-in classes java.util.Observer (interface) and java.util.Observable (class). In Java 9, they were officially marked @Deprecated due to severe architectural flaws:

  1. Observable Is a Class, Not an Interface: Because Java does not support multiple class inheritance, a class that already extends a domain base class cannot extend Observable.
  2. Lack of Generics & Type Safety: The update(Observable o, Object arg) method accepts raw Object, requiring fragile runtime downcasting.
  3. Clumsy State Management: Callers had to remember to invoke setChanged() before notifyObservers(). Forgetting this call silently suppressed all notifications.
  4. Outdated Concurrency: Internal synchronization relied on legacy Vector monitor locks, offering poor concurrency and no ordering control in modern multithreaded systems.

Standard Java Alternative: PropertyChangeSupport

For standard desktop applications and JavaBeans, Java SE provides PropertyChangeSupport:

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class UserAccount {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private String status = "PENDING";

    public void addPropertyChangeListener(PropertyChangeListener pcl) {
        support.addPropertyChangeListener(pcl);
    }

    public void setStatus(String newStatus) {
        String oldStatus = this.status;
        this.status = newStatus;
        // Propagates property name, old value, and new value to all listeners
        support.firePropertyChange("status", oldStatus, newStatus);
    }
}

Three Critical Production Resilience Hazards

1. ConcurrentModificationException During Unsubscription

If an observer attempts to unsubscribe (publisher.unsubscribe(this)) from inside its onEvent() callback while the publisher is iterating over a standard ArrayList, the JVM throws a ConcurrentModificationException.

  • Solution: Store listeners in a CopyOnWriteArrayList, or iterate over an immutable snapshot copy (new ArrayList<>(listeners)).

2. Observer Exception Isolation

If one observer throws an unhandled RuntimeException, the notification loop terminates abruptly, preventing subsequent observers from receiving the event.

  • Solution: Wrap each individual observer invocation in an isolated try-catch block inside the dispatch loop.

3. Memory Leaks (The “Lapsed Listener” Problem)

A long-lived publisher maintains a Strong Reference to every registered listener. If short-lived subscriber objects fail to explicitly unsubscribe when their lifecycle ends, the Garbage Collector cannot reclaim them because they remain reachable from the publisher’s GC Root.

  • Solution: Use WeakReference<EventListener> wrappers, or enforce deterministic lifecycle unregistration (e.g., using AutoCloseable or Spring bean lifecycle hooks).

🔴 Senior Level

Enterprise Event-Driven Architecture: Spring Application Events

In modern Spring Boot microservices, manual Observer implementations are replaced by Spring’s built-in event bus:

// 1. Immutable Domain Event (Java Record)
public record OrderPaidEvent(Long orderId, BigDecimal amount) {}

// 2. Publisher Service
@Service
public class PaymentProcessingService {
    private final ApplicationEventPublisher eventPublisher;

    public PaymentProcessingService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // Business logic modifying database
        eventPublisher.publishEvent(new OrderPaidEvent(orderId, amount));
    }
}

// 3. Event Listeners
@Component
public class OrderNotificationListener {

    // Asynchronous listener: executed in a background thread pool without blocking HTTP thread
    @Async
    @EventListener
    public void sendReceipt(OrderPaidEvent event) {
        // Sends email notification
    }

    // Executes ONLY AFTER database transaction commits successfully!
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void updateWarehouse(OrderPaidEvent event) {
        // Dispatches stock reservation only if payment is durably committed!
    }
}

Transaction Phases in @TransactionalEventListener:

  • AFTER_COMMIT (Default): Executes strictly after transaction commits. Prevents phantom external actions (like sending emails or triggering external shipping APIs) if the database transaction later rolls back!
  • BEFORE_COMMIT: Executes immediately before commit.
  • AFTER_ROLLBACK: Executes if the transaction rolls back (useful for compensation or audit logging).
  • AFTER_COMPLETION: Executes regardless of commit or rollback outcome.

Reactive Streams & Java 9 Flow API (Backpressure)

The classical Observer pattern is a pure push model without flow regulation. If a publisher generates 1,000,000 events/second while a slow consumer processes only 100/second, the consumer’s memory buffer rapidly exhausts, triggering OutOfMemoryError: Java heap space.

The Reactive Streams specification—implemented natively in java.util.concurrent.Flow (Java 9+) and libraries like Project Reactor / RxJava—solves this via Backpressure:

Publisher (Source)  <─── Subscriber.onSubscribe(Subscription)
      │
      │ ◄─── Subscription.request(10) [Subscriber asks: "Send me at most 10 items"]
      ├───── Subscriber.onNext(item_1)
      ├───── Subscriber.onNext(item_2)
      │      ... (exactly 10 items emitted)
      │
      │ ◄─── Subscription.request(5)  [Subscriber is ready: "Send me 5 more"]

Deadlock Hazard: Invoking “Alien Methods” Under Locks

Joshua Bloch (Effective Java, Item 79) warns against calling observer callbacks while holding an internal monitor lock:

// ❌ DANGEROUS: Alien method call inside synchronized block!
public synchronized void notifyObservers(Event event) {
    for (Listener l : listeners) {
        l.onEvent(event); // If the listener attempts to acquire another lock, DEADLOCK ensues!
    }
}

// ✅ SAFE: Open Calls (Invoking callbacks outside synchronization)
public void notifyObservers(Event event) {
    List<Listener> snapshot;
    synchronized (this) {
        snapshot = new ArrayList<>(listeners); // Brief lock to take snapshot
    }
    for (Listener l : snapshot) {
        l.onEvent(event); // Invoked without holding the lock!
    }
}

4 Tricky Questions

1. Why was java.util.Observable officially deprecated in Java 9?

Answer: java.util.Observable suffered from four fatal architectural flaws:

  1. It was implemented as an abstract class rather than an interface, making it impossible for classes with existing inheritance hierarchies to adopt it.
  2. It lacked generic typing, forcing callers to accept raw Object references and perform unchecked downcasting.
  3. It required invoking setChanged() before calling notifyObservers(), leading to silent bugs if omitted.
  4. Its synchronization relied on outdated Vector monitor locking, failing to satisfy modern concurrent ordering and thread-safety requirements.

2. What is the “Lapsed Listener” problem and how do Weak References mitigate it?

Answer: The Lapsed Listener problem occurs when a listener registers with a long-lived publisher (e.g., an application-scoped singleton) and retains a strong reference from the publisher’s listener collection. When the client code finishes with the listener, the Garbage Collector cannot reclaim it because an active GC root path exists from the publisher. Over time, zombie listeners accumulate in memory. By wrapping listeners in WeakReference<EventListener>, the garbage collector can reclaim the listener once all strong external references are gone. The publisher’s dispatch loop simply purges stale weak references that return null.


3. Why is @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) critical in database-backed applications?

Answer: A standard @EventListener executes synchronously within the active transaction boundary. If an event listener sends an external HTTP notification (such as an email or payment confirmation) and the database transaction subsequently fails and rolls back, the external message cannot be recalled, creating severe data inconsistency. The AFTER_COMMIT phase guarantees that the listener runs only after the database transaction has durably committed its changes to disk.


4. What constitutes an “Alien Method” in the Observer pattern, and how does calling it inside a synchronized block cause a deadlock?

Answer: An “Alien Method” is a method whose internal implementation is unknown to the caller—such as a user-supplied observer callback (listener.onEvent()). If a publisher invokes an alien method while holding its own monitor lock (synchronized (this)), the listener implementation might attempt to call back into another component or thread that requires acquiring another lock in reverse order. This creates a circular lock dependency, resulting in a fatal JVM thread deadlock. To prevent this, developers must use Open Calls: taking an immutable snapshot of listeners under lock and calling the alien methods outside the synchronized block.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Observer pattern decouples an event publisher from multiple subscribers using a one-to-many subscription model. In pure Java, legacy java.util.Observable was deprecated in Java 9 due to lack of generics and single-inheritance limitations; modern Java uses functional listeners over CopyOnWriteArrayList. In enterprise systems, Spring Events with @EventListener, @Async, and @TransactionalEventListener(phase = AFTER_COMMIT) provide decoupled, transaction-safe messaging. For high-volume streaming, the Java 9 Flow API (Reactive Streams) introduces Backpressure to prevent fast publishers from overwhelming slow consumers.”


Key Architectural Takeaways

  • Deprecation: Never use java.util.Observer in new code; use custom interfaces or Spring Events.
  • CopyOnWriteArrayList: Eliminates ConcurrentModificationException during unsubscription.
  • Lapsed Listener: Use WeakReference or explicit lifecycle hooks to prevent memory leaks.
  • Open Calls: Never call observer callbacks inside synchronized blocks (prevents deadlocks).
  • Spring AFTER_COMMIT: Essential to prevent dispatching false notifications on transaction rollbacks.
  • Backpressure: The Flow API allows consumers to regulate inbound event flow via request(n).

Red Flags & Anti-Patterns

  • 🚩 Using java.util.Observer / Observable in modern Java projects.
  • 🚩 Not isolating observer invocations in try-catch: One failing listener crashes the entire chain.
  • 🚩 Holding locks while notifying listeners: Invoking alien methods under monitor locks risks deadlocks.
  • 🚩 Sending transactional external side-effects (Email/SMS) inside standard @EventListener: Always use @TransactionalEventListener(phase = AFTER_COMMIT).