Back to Behavioral: Strategy, Observer, Command, State, Template
Q7Question 7 of 12

What is the Observer pattern, and what is its memory-leak risk

Key Concepts
  • Observer defines a one-to-many dependency so that when a subject changes state, all registered observers are notified automatically.
  • The lapsed-listener problem is the classic memory leak: observers that register but never unregister are held forever by the subject.
  • The subject's strong reference to the observer prevents garbage collection even if the rest of the application is done with it.
  • Fix with disciplined unsubscription: return a subscription handle that the caller closes in a finally block or try-with-resources.
  • Weak references are a safety net: the subject holds a `WeakReference` so forgotten observers can still be collected.
  • Spring's `ApplicationEvent` and the UI listener model are both Observer implementations.

Definition

Observer notifies many listeners when the subject changes, and its memory leak is the lapsed listener: observers that never unsubscribe are held forever by the subject.
  • Observer pattern – defines a one-to-many dependency so that when the subject changes state, all registered observers are notified automatically.
  • Subject – keeps a list of observers and calls them on each event; exposes subscribe() and unsubscribe() methods.
  • Observer – implements a callback interface (e.g., Consumer) and reacts to notifications.
  • Lapsed-listener problem – the classic memory leak where an observer registers but never unregisters, so the subject's strong reference prevents garbage collection.
  • Fix – return a subscription handle from subscribe() that the caller closes, or use weak references as a safety net.

National Bank of India's transfer desk keeps a call list of departments that want to know when a transfer completes: audit, SMS, and the dashboard. When a transfer finishes, the desk rings everyone on the list. The leak is like a department that signs up for the list, then shuts down, but never asks to be removed. The desk keeps dialling a dead office, and the list keeps growing with ghosts that can never be cleaned up.

Understanding the Concepts

What is the Observer pattern?

Observer defines a one-to-many dependency between a subject and its observers. The subject keeps a list of observers and notifies all of them when its state changes.

JAVA
package com.bank.observer;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class TransferPublisher {
    private final List<Consumer<TransferEvent>> listeners = new ArrayList<>();

    public AutoCloseable subscribe(Consumer<TransferEvent> listener) {
        listeners.add(listener);
        return () -> listeners.remove(listener); // subscription handle
    }

    public void publish(TransferEvent event) {
        listeners.forEach(l -> l.accept(event));
    }
}

Why does the Observer pattern exist?

Without Observer, the subject would need to know about every specific reaction: call the audit service, then the SMS service, then the dashboard. Adding a new reaction means editing the subject. Observer decouples the source from the reactions, so adding a new reaction is adding a new observer, never editing the subject.

JAVA
// Without Observer: subject coupled to every reaction (bad)
public void completeTransfer(TransferEvent event) {
    auditService.log(event);       // hard-coded
    smsService.send(event);        // hard-coded
    dashboardService.update(event); // hard-coded
    // Adding loyalty points means editing this method
}

// With Observer: subject decoupled (good)
publisher.subscribe(e -> auditService.log(e));
publisher.subscribe(e -> smsService.send(e));
publisher.subscribe(e -> dashboardService.update(e));
// Adding loyalty points: publisher.subscribe(e -> loyaltyService.award(e))

What is the lapsed-listener memory leak?

When an observer subscribes, the subject stores a strong reference to it. As long as that reference exists, the garbage collector cannot collect the observer. If the observer never unsubscribes, it lives as long as the subject, which is often the entire application lifetime.

JAVA
// Memory leak scenario
public void handleRequest(TransferPublisher publisher) {
    // Short-lived observer subscribes to long-lived publisher
    publisher.subscribe(e -> processForThisRequest(e));
    // Request ends, but the publisher still holds the reference
    // The lambda (and everything it captures) can never be collected
}
// After 10,000 requests: 10,000 dead observers in the list

How do you fix the memory leak?

The primary fix is returning a subscription handle that the caller closes when done. This makes unsubscription automatic and hard to forget.

JAVA
package com.bank.observer;

import java.util.function.Consumer;

public class SafeSubscriptionDemo {
    public static void main(String[] args) throws Exception {
        TransferPublisher publisher = new TransferPublisher();

        // Safe: subscription handle closed in try-with-resources
        try (AutoCloseable sub = publisher.subscribe(
                e -> System.out.println("Processing: " + e))) {
            publisher.publish(new TransferEvent(
                "SAV2001", "SAV3002",
                new java.math.BigDecimal("50000")));
        }
        // Observer is automatically removed when the block ends
    }
}
TEXT
Processing: TransferEvent[fromId=SAV2001, toId=SAV3002, amount=50000]

How do weak references help?

As a safety net, the subject can hold observers through WeakReference. When the observer has no other strong reference, the garbage collector can reclaim it, and the subject later purges the dead entry.

JAVA
package com.bank.observer;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;

public class WeakPublisher {
    private final List<WeakReference<Consumer<TransferEvent>>> listeners
        = new ArrayList<>();

    public void subscribe(Consumer<TransferEvent> listener) {
        listeners.add(new WeakReference<>(listener));
    }

    public void publish(TransferEvent event) {
        Iterator<WeakReference<Consumer<TransferEvent>>> it = listeners.iterator();
        while (it.hasNext()) {
            Consumer<TransferEvent> l = it.next().get();
            if (l == null) {
                it.remove(); // purge collected observer
            } else {
                l.accept(event);
            }
        }
    }
}

The trade-off is that the caller must keep a strong reference for as long as it wants notifications. A lambda with no other reference may be collected unexpectedly.

How does Spring use the Observer pattern?

Spring's ApplicationEvent and @EventListener are Observer. The application context is the subject, and @EventListener methods are observers.

JAVA
package com.bank.event;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class AuditListener {
    @EventListener
    public void onTransfer(TransferCompletedEvent event) {
        System.out.println("AUDIT: Transfer of INR "
                         + event.getAmount() + " recorded");
    }
}

Spring manages the listener lifecycle, so unsubscription happens automatically when the bean is destroyed.

What about observer failure isolation?

One slow or failing observer should not block the others. Notify defensively, catching exceptions per observer.

JAVA
public void publish(TransferEvent event) {
    for (Consumer<TransferEvent> l : listeners) {
        try {
            l.accept(event);
        } catch (Exception e) {
            log.warning("Observer failed: " + e.getMessage());
        }
    }
}

For heavy work, dispatch asynchronously so the subject does not wait for slow observers.

What are common mistakes?

The first mistake is subscribing without a plan to unsubscribe. Every subscribe must have a matching unsubscribe, ideally via a subscription handle.

JAVA
// Wrong: subscribe with no unsubscribe plan
publisher.subscribe(listener); // who removes this?

// Correct: subscription handle guarantees cleanup
try (var sub = publisher.subscribe(listener)) { ... }

The second mistake is modifying the observer list during notification, which causes ConcurrentModificationException. Use a copy of the list or a concurrent collection.

What should a senior engineer know?

The leak is not a flaw in the pattern but in its usage. The production symptom is a slowly growing heap and longer garbage-collection pauses. At National Bank of India, a long-lived price publisher fed short-lived request-scoped observers. Sessions subscribed observers and never removed them. Over a trading day, the publisher's list grew to tens of thousands of dead listeners with climbing heap usage. Pankaj fixed it by returning a subscription handle that each session closes, plus a weak-reference variant as a backstop, and the heap stayed flat.

JAVA
// Production monitoring: watch for growing observer lists
log.info("Observer count: " + listeners.size());
// If this number grows without bound, you have a leak

Visual Overview

DIAGRAM
  Observer Pattern
  ────────────────
  TransferPublisher (subject)
       │
       │  subscribe(listener)
       │
       ├── AuditListener
       ├── SmsListener
       ├── DashboardListener
       └── LoyaltyListener     ←── new reaction, zero changes to subject

  publish(event) → notify all listeners
DIAGRAM
  Memory Leak: Lapsed Listener
  ────────────────────────────
  Publisher.listeners = [
    listener1 (active),
    listener2 (dead, never unsubscribed),   ←── leak
    listener3 (dead, never unsubscribed),   ←── leak
    listener4 (active)
  ]
  Dead listeners hold strong references, preventing GC.

  Fix: subscription handle
  ────────────────────────
  try (var sub = publisher.subscribe(listener)) {
      // use
  } // auto-removed

Key Takeaway

The Observer pattern defines a one-to-many dependency where a subject keeps a list of observers and notifies all of them when its state changes. It decouples the event source from the reactions, so adding a new reaction means adding a new observer rather than editing the subject. Spring's ApplicationEvent and the UI listener model are both Observer implementations.

The memory-leak risk is the classic lapsed-listener problem. When an observer subscribes, the subject stores a strong reference in its list. If the observer never unsubscribes, it lives as long as the subject, which is usually the entire application lifetime. This is most dangerous when the subject is long-lived and the observers are short-lived, because the list silently accumulates dead observers, each holding captured state, and the heap grows until garbage-collection pauses lengthen.

The primary fix is treating every subscribe as owing an unsubscribe. Return a subscription handle from subscribe that the caller closes in a finally block or try-with-resources, so removal is automatic. As a safety net, hold observers through weak references so a forgotten observer with no other strong reference can still be collected. Also bound and audit the list, logging when it grows abnormally.

At National Bank of India, a long-lived price publisher fed short-lived request-scoped observers. Sessions subscribed and never removed them. Over a trading day the list grew to tens of thousands of dead listeners. Pankaj fixed it by returning a subscription handle that each session closes, plus a weak-reference backstop, and the heap stayed flat. The senior point: the leak is not a flaw in the pattern but in its usage, and pairing every subscribe with a guaranteed unsubscribe is what keeps Observer safe at scale.

Remember one sentence: Observer notifies many listeners on a state change, and the lapsed-listener memory leak is fixed by returning a subscription handle that the caller closes.

---

Share
Use ← → arrow keys to navigate