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:
ObservableIs a Class, Not an Interface: Because Java does not support multiple class inheritance, a class that already extends a domain base class cannot extendObservable.- Lack of Generics & Type Safety: The
update(Observable o, Object arg)method accepts rawObject, requiring fragile runtime downcasting. - Clumsy State Management: Callers had to remember to invoke
setChanged()beforenotifyObservers(). Forgetting this call silently suppressed all notifications. - Outdated Concurrency: Internal synchronization relied on legacy
Vectormonitor 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-catchblock 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., usingAutoCloseableor 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:
- It was implemented as an abstract class rather than an interface, making it impossible for classes with existing inheritance hierarchies to adopt it.
- It lacked generic typing, forcing callers to accept raw
Objectreferences and perform unchecked downcasting. - It required invoking
setChanged()before callingnotifyObservers(), leading to silent bugs if omitted. - Its synchronization relied on outdated
Vectormonitor 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.Observablewas deprecated in Java 9 due to lack of generics and single-inheritance limitations; modern Java uses functional listeners overCopyOnWriteArrayList. 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.Observerin new code; use custom interfaces or Spring Events. - CopyOnWriteArrayList: Eliminates
ConcurrentModificationExceptionduring unsubscription. - Lapsed Listener: Use
WeakReferenceor explicit lifecycle hooks to prevent memory leaks. - Open Calls: Never call observer callbacks inside
synchronizedblocks (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/Observablein 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).
Related Topics
- What is Iterator pattern — Traversing collections
- When to use Strategy — Behavioral pattern comparison
- Difference between State and Strategy — Behavioral state transitions
- What Proxy types exist — Proxies used in Spring event dispatching
- What pattern categories exist — Behavioral patterns overview