
Observer Design Pattern in Java – How It Works
The Observer design pattern is one of the fundamental behavioral patterns that defines a one-to-many dependency between objects, allowing multiple observers to automatically receive notifications when a subject’s state changes. Whether you’re building event-driven systems, implementing MVC architectures, or handling real-time data updates, understanding and implementing the Observer pattern is crucial for creating loosely coupled, maintainable Java applications. In this guide, you’ll learn how the Observer pattern works under the hood, get hands-on experience implementing it from scratch, explore real-world applications, and discover best practices to avoid common pitfalls that can lead to memory leaks and performance issues.
How the Observer Pattern Works
The Observer pattern establishes a subscription mechanism where objects (observers) can register to receive notifications about changes in another object (subject). Think of it like a newsletter subscription system – when the publisher releases new content, all subscribers automatically get notified.
The pattern consists of four main components:
- Subject (Observable): Maintains a list of observers and provides methods to add, remove, and notify them
- Observer: Defines an interface with an update method that gets called when the subject changes
- ConcreteSubject: Implements the subject interface and stores state that observers care about
- ConcreteObserver: Implements the observer interface and maintains a reference to the subject
Java actually provides built-in support for this pattern through the java.util.Observable
class and java.util.Observer
interface, though they’ve been deprecated since Java 9 in favor of more flexible alternatives like PropertyChangeListener
and reactive streams.
Step-by-Step Implementation Guide
Let’s build a complete Observer pattern implementation from scratch. We’ll create a stock price monitoring system where multiple displays update automatically when stock prices change.
First, define the Observer interface:
public interface Observer {
void update(String stockSymbol, double price);
}
Next, create the Subject interface:
public interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
Now implement the concrete subject (StockPriceMonitor):
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class StockPriceMonitor implements Subject {
private final List<Observer> observers;
private final ConcurrentHashMap<String, Double> stockPrices;
private String lastUpdatedSymbol;
private double lastUpdatedPrice;
public StockPriceMonitor() {
// Use thread-safe collections for concurrent access
observers = new CopyOnWriteArrayList<>();
stockPrices = new ConcurrentHashMap<>();
}
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(lastUpdatedSymbol, lastUpdatedPrice);
}
}
public void updateStockPrice(String symbol, double price) {
stockPrices.put(symbol, price);
lastUpdatedSymbol = symbol;
lastUpdatedPrice = price;
notifyObservers();
}
public double getPrice(String symbol) {
return stockPrices.getOrDefault(symbol, 0.0);
}
}
Create concrete observers for different display types:
public class MobileAppDisplay implements Observer {
private String displayName;
public MobileAppDisplay(String displayName) {
this.displayName = displayName;
}
@Override
public void update(String stockSymbol, double price) {
System.out.printf("[%s Mobile] Stock %s updated: $%.2f%n",
displayName, stockSymbol, price);
// Add mobile-specific formatting logic here
pushNotificationToDevice(stockSymbol, price);
}
private void pushNotificationToDevice(String symbol, double price) {
// Simulate push notification
System.out.printf("📱 Push notification: %s is now $%.2f%n", symbol, price);
}
}
public class WebDashboard implements Observer {
private String dashboardId;
public WebDashboard(String dashboardId) {
this.dashboardId = dashboardId;
}
@Override
public void update(String stockSymbol, double price) {
System.out.printf("[Web Dashboard %s] Updating %s: $%.2f%n",
dashboardId, stockSymbol, price);
// Update web dashboard via WebSocket
updateWebSocketClients(stockSymbol, price);
}
private void updateWebSocketClients(String symbol, double price) {
// Simulate WebSocket update
System.out.printf("🌐 WebSocket broadcast: {\"symbol\":\"%s\",\"price\":%.2f}%n",
symbol, price);
}
}
public class AlertSystem implements Observer {
private double alertThreshold;
public AlertSystem(double alertThreshold) {
this.alertThreshold = alertThreshold;
}
@Override
public void update(String stockSymbol, double price) {
if (price > alertThreshold) {
triggerAlert(stockSymbol, price);
}
}
private void triggerAlert(String symbol, double price) {
System.out.printf("🚨 ALERT: %s exceeded threshold! Current price: $%.2f%n",
symbol, price);
}
}
Here’s how to use the complete system:
public class StockMonitoringDemo {
public static void main(String[] args) {
// Create the subject
StockPriceMonitor monitor = new StockPriceMonitor();
// Create observers
MobileAppDisplay mobileApp = new MobileAppDisplay("TradingApp");
WebDashboard dashboard = new WebDashboard("MAIN");
AlertSystem alertSystem = new AlertSystem(150.0);
// Register observers
monitor.addObserver(mobileApp);
monitor.addObserver(dashboard);
monitor.addObserver(alertSystem);
// Simulate price updates
System.out.println("=== Stock Price Updates ===");
monitor.updateStockPrice("AAPL", 145.50);
monitor.updateStockPrice("GOOGL", 2650.75);
monitor.updateStockPrice("AAPL", 155.25); // This will trigger alert
// Remove an observer
monitor.removeObserver(mobileApp);
System.out.println("\n=== After removing mobile app ===");
monitor.updateStockPrice("TSLA", 220.80);
}
}
Real-World Examples and Use Cases
The Observer pattern appears everywhere in Java development. Here are some practical applications you’ll encounter:
Use Case | Subject | Observers | Example Implementation |
---|---|---|---|
GUI Event Handling | Button, TextField | ActionListener, FocusListener | Swing/JavaFX event systems |
Model-View-Controller | Model | View components | Spring MVC, web applications |
Microservices Events | Event Publisher | Event Handlers | Spring Events, Apache Kafka |
Database Changes | Entity/Repository | Cache managers, audit loggers | JPA Entity Listeners |
Configuration Updates | Configuration Manager | Application components | Spring @ConfigurationProperties |
Here’s a real-world example using Spring’s event system, which implements the Observer pattern:
// Custom event
public class UserRegistrationEvent extends ApplicationEvent {
private String userEmail;
private String userId;
public UserRegistrationEvent(Object source, String userEmail, String userId) {
super(source);
this.userEmail = userEmail;
this.userId = userId;
}
// getters...
}
// Event publisher (Subject)
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void registerUser(String email, String password) {
// Registration logic...
String userId = UUID.randomUUID().toString();
// Notify all observers
eventPublisher.publishEvent(
new UserRegistrationEvent(this, email, userId)
);
}
}
// Event listeners (Observers)
@Component
public class EmailNotificationService {
@EventListener
public void handleUserRegistration(UserRegistrationEvent event) {
sendWelcomeEmail(event.getUserEmail());
}
private void sendWelcomeEmail(String email) {
System.out.println("Sending welcome email to: " + email);
}
}
@Component
public class AnalyticsService {
@EventListener
@Async
public void handleUserRegistration(UserRegistrationEvent event) {
trackUserRegistration(event.getUserId());
}
private void trackUserRegistration(String userId) {
System.out.println("Tracking registration for user: " + userId);
}
}
Comparisons with Alternative Approaches
The Observer pattern isn’t the only way to handle notifications and decoupling. Here’s how it compares to other approaches:
Approach | Pros | Cons | Best For |
---|---|---|---|
Observer Pattern | Loose coupling, dynamic subscription, real-time updates | Memory leaks if not unsubscribed, potential cascading updates | GUI events, MVC, real-time systems |
Message Queues | Persistent, scalable, fault-tolerant | Added complexity, network overhead | Microservices, distributed systems |
Callbacks | Simple, direct | Tight coupling, callback hell | Simple async operations |
Reactive Streams | Backpressure handling, composable, thread-safe | Learning curve, complexity | High-throughput data processing |
Event Bus | Centralized, type-safe | Global state, testing difficulties | Component communication within applications |
For high-performance applications, consider using reactive streams with libraries like RxJava or Project Reactor:
// RxJava example
import io.reactivex.rxjava3.subjects.PublishSubject;
public class ReactiveStockMonitor {
private PublishSubject<StockPrice> stockPriceSubject;
public ReactiveStockMonitor() {
stockPriceSubject = PublishSubject.create();
}
public void updatePrice(String symbol, double price) {
stockPriceSubject.onNext(new StockPrice(symbol, price));
}
public void subscribe(Consumer<StockPrice> observer) {
stockPriceSubject
.observeOn(Schedulers.io())
.subscribe(observer);
}
// Filter and transform streams
public void subscribeToHighValueStocks(Consumer<StockPrice> observer) {
stockPriceSubject
.filter(stock -> stock.getPrice() > 100.0)
.map(stock -> new StockPrice(stock.getSymbol(),
stock.getPrice() * 1.1)) // Add 10% markup
.subscribe(observer);
}
}
Best Practices and Common Pitfalls
After working with the Observer pattern in production systems, here are the key practices that’ll save you from headaches:
Memory Management
The biggest issue you’ll face is memory leaks from unremoved observers. Always implement proper cleanup:
public class SafeObserverManager {
private final List<WeakReference<Observer>> observers;
public SafeObserverManager() {
observers = new ArrayList<>();
}
public void addObserver(Observer observer) {
// Clean up dead references first
cleanupDeadReferences();
observers.add(new WeakReference<>(observer));
}
public void notifyObservers(String data) {
Iterator<WeakReference<Observer>> iterator = observers.iterator();
while (iterator.hasNext()) {
WeakReference<Observer> ref = iterator.next();
Observer observer = ref.get();
if (observer == null) {
iterator.remove(); // Remove dead reference
} else {
observer.update(data);
}
}
}
private void cleanupDeadReferences() {
observers.removeIf(ref -> ref.get() == null);
}
}
Thread Safety
In multithreaded environments, use thread-safe collections and consider synchronization:
public class ThreadSafeSubject {
private final CopyOnWriteArrayList<Observer> observers;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public ThreadSafeSubject() {
observers = new CopyOnWriteArrayList<>();
}
public void notifyObservers(String data) {
readLock.lock();
try {
// CopyOnWriteArrayList handles concurrent iteration safely
observers.parallelStream().forEach(observer -> {
try {
observer.update(data);
} catch (Exception e) {
// Log error but continue notifying other observers
System.err.println("Observer notification failed: " + e.getMessage());
}
});
} finally {
readLock.unlock();
}
}
}
Performance Considerations
For systems with many observers or frequent updates, consider these optimizations:
public class OptimizedSubject {
private final ExecutorService notificationExecutor;
private final List<Observer> observers;
public OptimizedSubject() {
// Use a dedicated thread pool for notifications
notificationExecutor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
observers = new CopyOnWriteArrayList<>();
}
public void notifyObserversAsync(String data) {
// Batch notifications to reduce thread overhead
List<CompletableFuture<Void>> futures = observers.stream()
.map(observer -> CompletableFuture.runAsync(
() -> observer.update(data), notificationExecutor))
.collect(Collectors.toList());
// Optional: wait for all notifications to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> System.out.println("All observers notified"));
}
public void shutdown() {
notificationExecutor.shutdown();
try {
if (!notificationExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
notificationExecutor.shutdownNow();
}
} catch (InterruptedException e) {
notificationExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Common Pitfalls to Avoid
- Circular dependencies: Observer A updates Subject B, which notifies Observer C, which updates Subject A again
- Exception propagation: One failing observer shouldn’t break the entire notification chain
- Update storms: Rapid successive updates can overwhelm observers – consider debouncing
- Synchronous blocking: Long-running observer operations block the subject – use async notifications
- Observer resurrection: Creating new observers in response to notifications can cause infinite loops
Here’s a robust implementation that addresses these issues:
public class RobustSubject {
private final List<Observer> observers = new CopyOnWriteArrayList<>();
private final ScheduledExecutorService debounceExecutor =
Executors.newSingleThreadScheduledExecutor();
private volatile ScheduledFuture<?> pendingNotification;
private final Object notificationLock = new Object();
private String lastData;
public void updateWithDebounce(String data, long delayMs) {
synchronized (notificationLock) {
lastData = data;
// Cancel pending notification
if (pendingNotification != null) {
pendingNotification.cancel(false);
}
// Schedule new notification
pendingNotification = debounceExecutor.schedule(
this::executeNotification, delayMs, TimeUnit.MILLISECONDS
);
}
}
private void executeNotification() {
String dataToNotify;
synchronized (notificationLock) {
dataToNotify = lastData;
}
observers.parallelStream().forEach(observer -> {
try {
observer.update(dataToNotify);
} catch (Exception e) {
// Log but don't propagate exceptions
System.err.printf("Observer %s failed: %s%n",
observer.getClass().getSimpleName(), e.getMessage());
}
});
}
}
When implementing the Observer pattern in server environments, especially on VPS or dedicated servers, monitor memory usage and thread utilization carefully. The pattern can scale well, but improper implementation can lead to resource exhaustion under high load.
For additional reading on design patterns and Java best practices, check out the official Oracle Java tutorials and the Java Design Patterns repository on GitHub, which contains comprehensive examples and explanations of various design patterns including Observer.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.