Observer Pattern in Java — How, Why, and When to Use It
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.
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.
import java.util.ArrayList; import java.util.List; // --- The Observer contract --- // Any class that wants to receive updates must implement this. interface StockObserver { void onPriceChanged(String tickerSymbol, double newPrice); } // --- The Subject --- // Maintains a list of observers and notifies them on state change. class StockMarket { private final List<StockObserver> observers = new ArrayList<>(); private String tickerSymbol; private double currentPrice; public StockMarket(String tickerSymbol, double initialPrice) { this.tickerSymbol = tickerSymbol; this.currentPrice = initialPrice; } // Observers call this to sign up for notifications. public void addObserver(StockObserver observer) { observers.add(observer); } // Observers call this to unsubscribe — important to avoid memory leaks. public void removeObserver(StockObserver observer) { observers.remove(observer); } // Simulates a price update arriving from the exchange. public void updatePrice(double newPrice) { this.currentPrice = newPrice; notifyAllObservers(); // Automatically alerts every registered listener. } // Internal: loops through every registered observer and calls the contract method. private void notifyAllObservers() { for (StockObserver observer : observers) { observer.onPriceChanged(tickerSymbol, currentPrice); } } } // --- Concrete Observer 1: A trading dashboard --- class TradingDashboard implements StockObserver { @Override public void onPriceChanged(String tickerSymbol, double newPrice) { // Each observer decides what to DO with the update independently. System.out.printf("[Dashboard] %s price updated on screen: $%.2f%n", tickerSymbol, newPrice); } } // --- Concrete Observer 2: An automated alert system --- class PriceAlertSystem implements StockObserver { private final double alertThreshold; public PriceAlertSystem(double alertThreshold) { this.alertThreshold = alertThreshold; } @Override public void onPriceChanged(String tickerSymbol, double newPrice) { // This observer only acts when its own condition is met. if (newPrice > alertThreshold) { System.out.printf("[ALERT] %s crossed threshold! Current price: $%.2f%n", tickerSymbol, newPrice); } } } // --- Concrete Observer 3: An audit logger --- class AuditLogger implements StockObserver { @Override public void onPriceChanged(String tickerSymbol, double newPrice) { System.out.printf("[Audit Log] Price change recorded — %s: $%.2f%n", tickerSymbol, newPrice); } } // --- Entry point --- public class StockTicker { public static void main(String[] args) { StockMarket appleStock = new StockMarket("AAPL", 170.00); TradingDashboard dashboard = new TradingDashboard(); PriceAlertSystem alertSystem = new PriceAlertSystem(185.00); // Alert fires above $185 AuditLogger logger = new AuditLogger(); // All three observers register with the same subject. appleStock.addObserver(dashboard); appleStock.addObserver(alertSystem); appleStock.addObserver(logger); System.out.println("--- Price update 1 ---"); appleStock.updatePrice(180.50); // Below threshold — alert won't fire. System.out.println("--- Price update 2 ---"); appleStock.updatePrice(186.00); // Above threshold — alert fires. System.out.println("--- Dashboard unsubscribes ---"); appleStock.removeObserver(dashboard); // Dashboard opts out. System.out.println("--- Price update 3 ---"); appleStock.updatePrice(190.00); // Dashboard no longer receives this. } }
[Dashboard] AAPL price updated on screen: $180.50
[Audit Log] Price change recorded — AAPL: $180.50
--- Price update 2 ---
[Dashboard] AAPL price updated on screen: $186.00
[ALERT] AAPL crossed threshold! Current price: $186.00
[Audit Log] Price change recorded — AAPL: $186.00
--- Dashboard unsubscribes ---
--- Price update 3 ---
[ALERT] AAPL crossed threshold! Current price: $190.00
[Audit Log] Price change recorded — AAPL: $190.00
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.
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; // --- The Subject using Java's built-in PropertyChangeSupport --- // This is ideal for JavaBean-style domain objects. class UserProfile { // PropertyChangeSupport does the heavy lifting of managing listeners. private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); private String username; private String emailAddress; public UserProfile(String username, String emailAddress) { this.username = username; this.emailAddress = emailAddress; } // Standard registration method expected by the Java Beans convention. public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } public void setEmailAddress(String newEmailAddress) { String previousEmail = this.emailAddress; this.emailAddress = newEmailAddress; // Fires the event with the property name, old value, AND new value. // Observers get context — not just 'something changed'. changeSupport.firePropertyChange("emailAddress", previousEmail, newEmailAddress); } public void setUsername(String newUsername) { String previousUsername = this.username; this.username = newUsername; changeSupport.firePropertyChange("username", previousUsername, newUsername); } public String getUsername() { return username; } public String getEmailAddress() { return emailAddress; } } // --- Observer 1: Sends a verification email when the email address changes --- class EmailVerificationService implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent event) { // Only react to the property we care about — ignore everything else. if ("emailAddress".equals(event.getPropertyName())) { System.out.printf("[Email Service] Verification sent to new address: %s (was: %s)%n", event.getNewValue(), event.getOldValue()); } } } // --- Observer 2: Audit log that tracks ALL profile changes --- class ProfileAuditLog implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent event) { // This one listens to every property — the event tells it which one changed. System.out.printf("[Audit] Field '%s' changed: '%s' → '%s'%n", event.getPropertyName(), event.getOldValue(), event.getNewValue()); } } public class UserProfileObserver { public static void main(String[] args) { UserProfile profile = new UserProfile("jsmith", "john@oldmail.com"); EmailVerificationService emailService = new EmailVerificationService(); ProfileAuditLog auditLog = new ProfileAuditLog(); profile.addPropertyChangeListener(emailService); profile.addPropertyChangeListener(auditLog); System.out.println("--- Changing email address ---"); profile.setEmailAddress("john@newmail.com"); System.out.println("--- Changing username ---"); profile.setUsername("johnsmith"); System.out.println("--- Setting same email again (no event fired) ---"); // PropertyChangeSupport is smart: it won't fire if old == new value. profile.setEmailAddress("john@newmail.com"); } }
[Email Service] Verification sent to new address: john@newmail.com (was: john@oldmail.com)
[Audit] Field 'emailAddress' changed: 'john@oldmail.com' → 'john@newmail.com'
--- Changing username ---
[Audit] Field 'username' changed: 'jsmith' → 'johnsmith'
--- Setting same email again (no event fired) ---
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.
import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; // A thread-safe Subject using CopyOnWriteArrayList. // Safe for concurrent registrations and notifications. class SensorDataFeed { // CopyOnWriteArrayList: reads are lock-free, writes copy the internal array. // Perfect for observer lists where reads (notifications) dominate writes (registrations). private final List<SensorObserver> observers = new CopyOnWriteArrayList<>(); private volatile double latestReading; // volatile ensures visibility across threads. public void register(SensorObserver observer) { observers.add(observer); } public void unregister(SensorObserver observer) { observers.remove(observer); // Safe to call from any thread. } public void publishReading(double sensorValue) { this.latestReading = sensorValue; // Even if another thread adds/removes an observer mid-iteration, // CopyOnWriteArrayList won't throw ConcurrentModificationException. for (SensorObserver observer : observers) { observer.onNewReading(sensorValue); } } } interface SensorObserver { void onNewReading(double value); } // A short-lived observer that properly unregisters itself when done. class TemperatureMonitor implements SensorObserver { private final SensorDataFeed feed; private final String monitorId; public TemperatureMonitor(String monitorId, SensorDataFeed feed) { this.monitorId = monitorId; this.feed = feed; feed.register(this); // Register on construction. } @Override public void onNewReading(double value) { System.out.printf("[%s] Temperature reading: %.1f°C%n", monitorId, value); } // Always provide a cleanup method — this prevents the memory leak. public void shutdown() { feed.unregister(this); System.out.printf("[%s] Unregistered from feed.%n", monitorId); } } public class ThreadSafeEventBus { public static void main(String[] args) throws InterruptedException { SensorDataFeed temperatureSensor = new SensorDataFeed(); TemperatureMonitor kitchenMonitor = new TemperatureMonitor("KitchenSensor", temperatureSensor); TemperatureMonitor serverRoomMonitor = new TemperatureMonitor("ServerRoom", temperatureSensor); // Simulate concurrent sensor readings from a background thread. ExecutorService sensorThread = Executors.newSingleThreadExecutor(); sensorThread.submit(() -> { temperatureSensor.publishReading(22.5); temperatureSensor.publishReading(23.1); }); sensorThread.shutdown(); sensorThread.awaitTermination(2, TimeUnit.SECONDS); // Simulate the kitchen monitor going offline — must unregister to avoid memory leak. kitchenMonitor.shutdown(); System.out.println("--- Reading after kitchen monitor unregistered ---"); temperatureSensor.publishReading(24.0); // Only serverRoomMonitor receives this. } }
[ServerRoom] Temperature reading: 22.5°C
[KitchenSensor] Temperature reading: 23.1°C
[ServerRoom] Temperature reading: 23.1°C
[KitchenSensor] Unregistered from feed.
--- Reading after kitchen monitor unregistered ---
[ServerRoom] Temperature reading: 24.0°C
| Aspect | Custom Observer (Roll Your Own) | PropertyChangeSupport (java.beans) |
|---|---|---|
| Setup complexity | Minimal — just an interface and a list | Slightly more — requires PropertyChangeEvent handling |
| Event payload | You define exactly what data observers receive | Always fires old value + new value + property name |
| Thread safety | Not by default — you must use CopyOnWriteArrayList | Not thread-safe by default for compound operations |
| Same-value filtering | No — fires even if value didn't change | Yes — automatically skips notification if old == new |
| Best use case | Domain-specific events with custom data shapes | JavaBean-style domain objects with named properties |
| Deprecated risk | None | None (java.util.Observable is deprecated, not this) |
| Coupling level | Observer interface only — very loosely coupled | PropertyChangeListener interface — still loosely coupled |
| IDE/framework support | Manual wiring | Strong support in Swing, JavaFX (older APIs) |
🎯 Key Takeaways
- The Observer Pattern decouples the Subject from its observers using an interface contract — the Subject never imports or knows about concrete observer types, which is what makes it genuinely extensible.
- Java's java.util.Observable is deprecated and dead — use PropertyChangeSupport for JavaBean objects or roll your own interface-based implementation for domain-specific events.
- Always swap ArrayList for CopyOnWriteArrayList in any observer list that might be accessed from multiple threads — the performance trade-off is almost always worth the safety.
- Memory leaks are the silent killer of Observer implementations — every register() call must have a paired unregister() call in a predictable lifecycle method, or you'll leak objects indefinitely.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Forgetting to call removeObserver when a short-lived component is destroyed — Symptom: memory usage grows steadily over time (classic memory leak); no exception is thrown so it's hard to spot. Fix: always pair registration with a cleanup path — a close(), dispose(), or lifecycle callback that explicitly calls removeObserver. In Android this is onDestroy(), in Spring it's @PreDestroy.
- ✕Mistake 2: Using a plain ArrayList for the observer list in a multi-threaded context — Symptom: ConcurrentModificationException thrown during iteration when another thread adds or removes an observer, or worse, silent corruption with no exception at all. Fix: replace ArrayList with CopyOnWriteArrayList. It's thread-safe for iteration and requires no manual synchronisation for typical observer workloads.
- ✕Mistake 3: Pushing too much data in the notification (the 'push model' overload) — Symptom: the update() method signature balloons with parameters, and observers receive data they don't need, creating tight coupling between subject and observers. Fix: use the 'pull model' — send a minimal notification (or just a reference to the subject) and let each observer query only the state it actually needs. This keeps the interface stable even as the subject evolves.
Interview Questions on This Topic
- QWhat's the difference between the push model and pull model in the Observer Pattern, and when would you choose one over the other?
- QHow would you make an Observer implementation thread-safe in Java, and why is using ArrayList for the observer list dangerous in a concurrent environment?
- QHow does the Observer Pattern relate to the Event-Driven architecture and the Publish-Subscribe pattern — are they the same thing?
Frequently Asked Questions
Is java.util.Observable still usable in modern Java?
It was deprecated in Java 9 and should not be used in new code. The core problem is that Observable is a class, not an interface, so your subject must extend it — and since Java doesn't support multiple inheritance, that's often impossible. Use PropertyChangeSupport or roll your own interface-based solution instead.
What is the difference between Observer Pattern and Pub/Sub Pattern?
In the Observer Pattern, observers register directly with the subject — they know about each other through the interface. In Pub/Sub, a message broker sits between publishers and subscribers, meaning publishers and subscribers have zero direct knowledge of each other. Pub/Sub scales better across systems; Observer is simpler and more appropriate for in-process, single-application use cases.
Can one observer register with multiple subjects at the same time?
Yes, absolutely — and this is actually a common real-world scenario. A single audit logger, for example, might register with a UserService, an OrderService, and a PaymentService simultaneously. Each subject holds its own list of observers independently. The observer just needs to handle the onPriceChanged or propertyChange call correctly regardless of which subject triggered it, which is why the event payload (like PropertyChangeEvent's getSource()) often includes a reference to the originating subject.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.