Java Observer Pattern — Missing removeObserver Causes OOM
A missing removeObserver caused a 6-hour memory leak in Java Observer Pattern, leading to OOM.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Observer Pattern defines one-to-many dependency between objects
- Subject maintains a list of observers and notifies them on state change
- Observers implement a common interface to stay decoupled
- Performance: notification is O(n) where n = number of observers; use CopyOnWriteArrayList to avoid ConcurrentModificationException
- Production risk: failing to unregister observers causes memory leaks; every subscribe must have paired unsubscribe
- Biggest mistake: using java.util.Observable (deprecated) instead of an interface-based design
Imagine you subscribe to a YouTube channel. You don't keep refreshing the page waiting for videos — YouTube just notifies you when something new drops. The Observer Pattern works exactly like that: one object (the channel) keeps a list of subscribers and pings all of them automatically whenever something important changes. You're the observer, YouTube is the subject, and the notification is the update.
Every non-trivial application has objects that need to react when something else changes. A stock price ticks up and three dashboards need to refresh. A user submits a form and an email service, a logging system, and an analytics tracker all need to know. Without a clean pattern for this, you end up with tightly coupled code where every component manually pokes every other component — and that becomes a maintenance nightmare fast.
The Observer Pattern solves this by inverting the dependency. Instead of Component A calling Component B, C, and D directly, A just announces 'something changed' and any component that cares can listen. The components that care register themselves as observers. A doesn't know who's listening, and the listeners don't need to know how A works internally. That separation is everything.
By the end of this article you'll be able to build a working Observer implementation from scratch in Java, understand when Java's built-in tools (like PropertyChangeSupport) are a better choice than rolling your own, spot the common mistakes that turn a clean pattern into a memory leak, and answer the Observer questions that come up in senior Java interviews.
Why Observer Pattern Without removeObserver Leaks Memory
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. In Java, this typically means a Subject maintains a list of Observer references and iterates over them on each state change. The core mechanic is a subscription list — observers register via addObserver and deregister via removeObserver.
In practice, the Subject holds strong references to observers. If an observer is no longer needed but remains registered, it becomes an unintentional strong reference root. This prevents the observer (and often its entire object graph) from being garbage collected. Over time, the observer list grows unbounded, and notification cycles become O(n) with n being stale observers. The result: heap exhaustion and OutOfMemoryError.
Use this pattern when you need loose coupling between a subject and multiple dependents that must stay in sync — UI event systems, messaging buses, or reactive streams. But in Java, the default Observable/Observer classes are notoriously dangerous because they lack a built-in weak reference mechanism. Always pair registration with guaranteed deregistration, or switch to WeakReference-based implementations.
The Core Mechanic — Subject, Observer, and the Contract Between Them
The Observer Pattern has three moving parts: the Subject (also called Observable), the Observer interface, and the concrete observers that do something with the notification.
The Subject holds a list of registered observers and is responsible for notifying them when its state changes. It doesn't care what they do with that notification — it just calls a single agreed-upon method. That agreed-upon method is the Observer interface, and it's the contract that keeps everything decoupled.
The concrete observers implement that interface. Each one registers itself with the subject and defines its own behaviour inside the update method. They're completely independent of each other.
Why an interface? Because the Subject should be able to hold observers of any type — a UI component, a logger, a third-party analytics SDK — without importing any of their classes. The interface is the only thing they share. This is the Open/Closed Principle in action: you can add a new observer type without touching the Subject at all.
Observer Pattern UML — Publisher, Subscriber, and Concrete Subscriber
The UML diagram below shows the static structure of the Observer Pattern. The key actors are the Subject (Publisher), the Observer interface (Subscriber), and the ConcreteObserver (ConcreteSubscriber) that implements the interface. The Subject maintains a list of observers and allows them to attach or detach. The notification method iterates over the list and calls update() on each. This design completely decouples the Subject from the ConcreteObserver; only an interface is shared. In Java, you often rename these to EventPublisher vs EventListener, but the pattern remains identical.
Java's Built-In Observer Tools — PropertyChangeSupport vs Roll Your Own
Java has had observer-style tooling baked in for decades. The original java.util.Observable class and java.util.Observer interface shipped in Java 1.0 — but they were deprecated in Java 9 and you should not use them. Observable is a class, not an interface, which means your subject must extend it and Java only allows single inheritance. That's a design dead-end.
The better built-in option is PropertyChangeSupport, which lives in java.beans. It's lightweight, thread-safe (for single property changes), and widely used in Java desktop frameworks. It fires an event that carries the property name, the old value, and the new value — which is far more informative than a generic 'something changed' ping.
For reactive, async, or stream-based scenarios, Java 9 introduced the Flow API (java.util.concurrent.Flow) which implements the Reactive Streams specification. Libraries like RxJava and Project Reactor build on this mental model.
Knowing which tool to reach for matters. Roll your own for domain-specific, lightweight needs. Use PropertyChangeSupport for Java Bean-style objects. Use Flow or Reactor when you're dealing with async streams, backpressure, or large event volumes.
Thread Safety and Memory Leaks — The Two Observer Traps in Production
The Observer Pattern looks clean in tutorials but has two sharp edges that bite in production: thread safety and memory leaks.
If multiple threads call addObserver, removeObserver, and notifyAllObservers concurrently on a plain ArrayList, you'll get ConcurrentModificationException or worse — silent data corruption. The fix is to use CopyOnWriteArrayList instead of ArrayList for your observer list. It's slightly slower on writes (it copies the list on every mutation) but reads and iterations — which are far more frequent in observer scenarios — are completely lock-free and safe.
Memory leaks are sneakier. If an observer registers with a long-lived subject but never explicitly removes itself, the subject holds a reference to the observer forever. The garbage collector can't reclaim it. In a UI application this means every time a screen is opened and closed, a new observer is added and the old one lingers. After enough cycles, memory fills up. The fix is always to pair addObserver with a cleanup path — a close(), dispose(), or onDestroy() method that calls removeObserver. If you're working with Java's WeakReference, you can also store observers as weak references so the GC can collect them if nothing else holds them — but this requires careful design.
update() method, and the subject's lock is still held, you're stuck. The CopyOnWriteArrayList approach avoids this entirely because no lock is held during iteration.Push vs Pull Model — Which One Should You Use?
When implementing the Observer Pattern, you have two models for delivering data: push or pull.
In the push model, the subject sends detailed data to observers as part of the notification. The observer's update method receives all the information it might need. This is simple and fast for observers that need all the data, but it creates tighter coupling — if the subject's data shape changes, every observer interface must change.
In the pull model, the subject sends only a minimal notification (or just a reference to itself). The observer then calls getter methods on the subject to retrieve only the data it actually needs. This is more flexible and keeps the interface stable even as the subject evolves, but it requires the observer to know the subject's interface.
Which one should you pick? If your observers need most of the subject's state and the data shape is stable, push is fine. If observers need different subsets of data or the subject's state evolves often, pull is better. Many production systems use a hybrid: push a small event object that includes a reference to the subject and maybe a change type, then let observers pull details as needed.
- Push is simpler for observers that need all data — but brittle when data shape changes.
- Pull keeps the interface minimal and stable — observers can evolve independently.
- Hybrid (push a small event with a reference) is the most production-friendly pattern.
- Rule: start with pull; only switch to push if profiling shows performance gain.
Push vs Pull Notification Model — Comparison Table
| Aspect | Push Model | Pull Model |
|---|---|---|
| Data delivery | Subject sends all relevant data in notification | Subject sends only a reference or minimal event; observer queries needed data |
| Interface stability | Changes in subject data shape require changes in all observer interfaces | Observer interface remains stable; subject can add new data without breaking observers |
| Coupling | Tight: observers depend on specific data structure | Loose: observers depend on subject's getter methods |
| Performance | Faster for observers that need all data; no additional method calls | Slightly slower due to additional getter calls; observers may fetch data they don't need |
| Flexibility | Low: observers must accept all data even if not needed | High: each observer selects only what it needs |
| Best for | Stable data shapes, observers that use most of the data | Evolving data shapes, observers with varying data needs |
| Real-world example | Stock ticker update pushes full price object | PropertyChangeListener pulls old/new values from event |
Spring's Event Framework — @EventListener and ApplicationEvent as Built-in Observer
Spring Framework provides a first-class event system that implements the Observer Pattern at the application level. You define events by extending ApplicationEvent (or using generic payload events), and you define listeners by annotating methods with @EventListener. The ApplicationContext acts as the subject, managing listeners and publishing events to all registered listeners.
Spring's event system is synchronous by default (listeners run in the caller's thread) but can be made asynchronous with @Async. It also supports event hierarchy, conditional listeners (e.g., only react to certain event types), and transactional events that only fire after a successful commit.
This is a production-ready implementation: thread-safe, decoupled, and integrated with Spring's lifecycle. You should prefer it over rolling your own event system inside a Spring application. Even if you're not using Spring, the pattern of a central event bus with annotated listeners is worth understanding.
Real-Life Applications of the Observer Pattern
The Observer Pattern appears in countless real-world systems. Understanding these concrete examples helps you recognise when the pattern is the right fit and how to implement it correctly.
- Social Media Notifications: When you post a photo, your followers receive a notification. Each follower is an observer; the social network is the subject. The subject maintains a list of followers (observers) and pushes a notification when new content appears. This is a classic push-model observer in action.
- Stock Market Dashboards: A stock exchange publishes price updates. Multiple dashboards, alert systems, and analytics tools subscribe to these updates. When a stock price changes, all subscribers are notified. The subject (price feed) doesn't know what each subscriber does — it just calls a method on the observer interface. This is the exact example used throughout this article.
- GUI Event Listeners: In Java Swing or JavaFX, every button click, mouse move, or key press is handled via observer-like patterns. A Button has a list of ActionListener objects. When the button is clicked, it iterates over the list and calls actionPerformed on each listener. This is the Observer Pattern applied to UI events.
- Weather Monitoring Systems: A weather station collects data (temperature, humidity, pressure) and broadcasts updates to multiple display devices. Each display (current conditions, forecast, statistics) is an observer that pulls the data it needs from the weather station. This is a classic pull-model observer, often used in textbooks to introduce the pattern.
In each case, the key benefit is decoupling: the subject (social network, price feed, button, weather station) can evolve independently of the observers. Adding a new observer requires no changes to the subject.
Testing Observers — How to Verify Notification Contracts
Observers are often side-effect-heavy — they send emails, write logs, update UIs. That makes them tricky to test. You don't want to actually send emails in your unit tests, but you do need to verify that the observer was called with the right data.
Use mocking to verify that the observer's method was invoked. Mock the observer interface, register it with the subject, trigger a state change, and assert that the mock's method was called with expected arguments.
For integration tests, you can use a real observer that records calls (like a spy) and then check the recorded data. This is useful for verifying multi-observer scenarios or complex event flows.
- Observer is called exactly once per state change.
- Observer receives correct data (push) or can pull correct data (pull).
- Observer is NOT called after being removed.
- Thread safety: concurrent registrations and notifications don't corrupt the observer list.
- Memory leaks: after removing all observers, the subject holds no references (test with WeakReference).
Practice Exercises to Master the Observer Pattern
The following exercises will solidify your understanding by applying the pattern in different contexts. Each exercise builds on the core concepts: subject, observer interface, registration, notification, and lifecycle cleanup.
- Event Bus: Build a simple in-memory event bus that allows multiple subscribers to register for specific event types (e.g., UserCreated, OrderShipped). The bus should support wildcard subscriptions (e.g., subscribe to all events). Ensure thread safety and provide a mechanism to unsubscribe. Test with concurrent publishers and subscribers.
- Stock Ticker: Implement a stock ticker system where multiple display components (price table, chart, alert) subscribe to a single stock price feed. Use the pull model: the subject only notifies that a price changed, and each display pulls the relevant data. Add a scenario where a display can unsubscribe mid-stream and verify it stops receiving updates.
- Pub-Sub System: Create a publish-subscribe system with a message broker as an intermediary. Publishers push messages to the broker, and subscribers receive messages based on topic filters. This is a more advanced exercise that adds a broker layer on top of the Observer Pattern. Implement it in-memory first, then consider adding a persistent queue.
- Weather Station: Implement the classic weather station example. The WeatherSubject holds temperature, humidity, and pressure. Multiple display observers (CurrentConditions, Statistics, Forecast) pull the data they need. Add a new observer (HeatIndex) without modifying the subject or existing observers. Ensure that removing an observer doesn't affect others.
- Java Virtual Machine Monitoring: Write a simple JVM monitor that uses the Observer Pattern to track garbage collection events. The subject polls JMX beans and notifies observers when GC metrics change. Use CopyOnWriteArrayList for thread safety and add a test that simulates GC events and verifies the observer receives the expected metrics. Implement proper cleanup so observers don't leak.
The Lapsed Listener Problem — Why Your App Is Slowly Eating Memory
You've seen it in production. The heap graph creeps up. GC cycles get longer. Then, out of nowhere, the OOM killer takes down your service. The root cause? An observer that forgot to unsubscribe.
This isn't theory. Every senior has debugged a memory leak where a listener held a reference to a dead component, keeping it alive in the old generation. The Subject still holds a strong reference to your Observer. Your Observer holds a back-reference to the owning class. Now you have a retention chain that prevents GC.
The fix isn't just adding removeObserver(). It's understanding who controls unsubscription. Patterns like using WeakReferences with ObserverList, or lifecycle callbacks that guarantee removal, are production necessities. In Spring, @EventListener automatically manages this. In raw Java, you must.
Here's the uncomfortable truth: if your Observer outlives its usefulness, and your Subject is long-lived, you are creating slow, creeping death. Fix it before your pager goes off.
close())Key Concepts Unpacked — What the UML Diagrams Don't Tell You
Textbooks show you a neat UML: Subject, Observer, concrete implementations. They pretend the contract is clean. It isn't.
Here's what they skip:
- Notification order matters. If you have observers that depend on other observers' side effects, you're in a garbage fire. Never assume ordering unless your framework guarantees it (e.g., @Order in Spring).
- Synchronous vs. async. Standard Observer is synchronous — the notify loop blocks the Subject. If an Observer blocks (I/O, slow computation), your Subject stalls. Push notifications onto a thread pool, or use an event bus, or accept the latency profile.
- The Subject's state during notification. You're iterating a live list. If an observer, inside its
update(), calls removeObserver() — you've just modified the collection you're iterating. Welcome to ConcurrentModificationException. Use CopyOnWriteArrayList or iterate over a snapshot.
These aren't academic. They're the bugs that appear at 3AM on Black Friday. Know them before you ship.
The Observer Pattern Overview — Why Most Descriptions Miss the Real Point
The Observer pattern lets one object (the subject) broadcast state changes to multiple dependents (observers) without knowing who they are. That decoupling is the entire reason it exists — not the notification itself, but the fact that the subject doesn't import, instantiate, or even name its observers.
Most tutorials start with a weather station example. Fine for teaching inheritance, terrible for production thinking. In real systems, the subject is often a shared resource — a cache, a config store, a user session — and observers are UI components, analytics pipelines, or logging services. The pattern buys you the ability to add or remove those dependents at runtime without touching the subject's code.
The real question isn't 'how do I notify observers?' It's 'how do I keep this subject from knowing who's watching it while still telling them what changed?' Answer: a list of interfaces. That list is both your power and your landmine — memory leaks live there.
Diagrammatic Representation of the Lapsed Listener Problem — See the Leak
The lapsed listener problem looks simple on paper: a listener holds a reference to the subject, the subject holds a reference to the listener, and neither ever cleans up. But the diagram tells the real story — it's not just a circle, it's a rooted GC path.
Imagine a UI panel that registers itself as an observer on a user session object. The panel is closed, but the session still holds a reference to the panel through the observer list. The panel can't be garbage collected because the session — probably held by a static context or application scope — keeps it alive. That's not a loop; that's a chain from a GC root straight into abandoned memory.
The fix is symmetric lifecycle management: every register() must be paired with an unregister(). If you're using anonymous lambdas or inner classes, you can't even remove them because you don't hold the reference. Store the listener reference explicitly, or use WeakReferences if you accept the complexity trade-off.
close() or dispose(). Your future self (and your Ops team) will thank you.attach() with a detach(), or use a lifecycle-aware framework.The Silent Observer Leak That Crashed Our Trading Platform
- Every addObserver must have a paired removeObserver in a deterministic lifecycle method (close, dispose, onDestroy).
- For long-lived subjects, consider using weak references (e.g., WeakHashMap) to allow GC to clean up observers that are no longer reachable from other roots.
- Monitor heap usage and observer list size in production (e.g., via JMX or custom metrics) to catch leaks early.
jmap -histo:live <pid> | grep -E 'Observer|Listener'jcmd <pid> GC.class_stats | awk '{print $1, $2, $3}'| File | Command / Code | Purpose |
|---|---|---|
| StockTicker.java | interface StockObserver { | The Core Mechanic |
| UserProfileObserver.java | class UserProfile { | Java's Built-In Observer Tools |
| ThreadSafeEventBus.java | class SensorDataFeed { | Thread Safety and Memory Leaks |
| PushVsPull.java | interface ObserverPush { | Push vs Pull Model |
| SpringEventExample.java | public class OrderPlacedEvent extends ApplicationEvent { | Spring's Event Framework |
| ObserverTest.java | class ObserverTest { | Testing Observers |
| LapsedListenerDemo.java | class EventSource { | The Lapsed Listener Problem |
| ConcurrentModificationPitfall.java | interface Observer { void update(); } | Key Concepts Unpacked |
| OverviewObserver.java | interface StateChangeListener { | The Observer Pattern Overview |
| LeakyListener.java | class Session { | Diagrammatic Representation of the Lapsed Listener Problem |
Key takeaways
register() call must have a paired unregister() call in a predictable lifecycle method, or you'll leak objects indefinitely.Interview Questions on This Topic
What's the difference between the push model and pull model in the Observer Pattern, and when would you choose one over the other?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Advanced Java. Mark it forged?
11 min read · try the examples if you haven't