Simple explanation for Observer Pattern
Situation: You Built a Stock Price Updater
You have a StockPriceService that updates the latest price for a stock.
Initially, you just print the new price to the console. All good.
stockPriceService.update("AAPL", 174.22);
Then Product Demands Pile Up
- Send price updates to mobile app
- Send price updates to dashboard UI
- Send price updates to Slack channel
- Store it in database
- Push to analytics service
Now your method starts looking like this:
void updatePrice(String stock, double price) {
logToConsole();
updateUI();
notifyMobileApp();
saveToDB();
sendToSlack();
triggerAnalytics();
}
What was once clean… is now cluttered chaos. Every new “listener” = more hardcoded logic.
What’s the Problem?
- Your StockPriceService knows way too much about who’s listening.
- It’s tightly coupled to every downstream consumer.
- Adding/removing listeners means editing this one method → violates Open/Closed Principle
- Testing becomes painful
- No flexibility - you can’t add or remove subscribers at runtime
How Observer Pattern Saves You
Observer says: “Let the stock price service focus on updates. Anyone who cares about the change can subscribe and be notified automatically.”
So you:
- Define an Observer interface (e.g., onPriceUpdate)
- Make all interested parties (UI, Slack, DB, etc.) implement this interface
- In StockPriceService, you just maintain a list of observers
- When price updates → loop and notify all
No hardcoded dependencies. Adding/removing listeners becomes plug-and-play.
Implementation
// 1. Observer interface
interface PriceObserver {
void onPriceUpdate(String stock, double price);
}
// 2. Observable subject
class StockPriceService {
private final List<PriceObserver> observers = new ArrayList<>();
public void subscribe(PriceObserver observer) {
observers.add(observer);
}
public void unsubscribe(PriceObserver observer) {
observers.remove(observer);
}
public void updatePrice(String stock, double price) {
System.out.println("Updating price for " + stock + ": $" + price);
for (PriceObserver obs : observers) {
obs.onPriceUpdate(stock, price);
}
}
}
// 3. Concrete observer
class MobileApp implements PriceObserver {
public void onPriceUpdate(String stock, double price) {
System.out.println("Mobile received: " + stock + " @ $" + price);
}
}
// Usage
StockPriceService service = new StockPriceService();
service.subscribe(new MobileApp());
When to Use Observer?
- When multiple parts of your app react to the same event
- When you want to decouple the source from its listeners
- When you need a pub-sub model (event buses, UI updates, streams)
Benefits
- Decoupling: Subject doesn’t need to know details about its observers
- Open/Closed: Add new subscribers without modifying the subject
- Dynamic relationships: Add/remove observers at runtime
- One-to-many: A single subject can notify multiple observers
- Loose coupling: Publishers and subscribers can vary independently
Real-World Examples
- Event handling in UI frameworks
- Message queues and event buses
- Notification systems
- Real-time data monitoring
- Reactive programming frameworks
Drawbacks
- If not managed correctly, can lead to unexpected updates
- Potential memory leaks if observers aren’t unsubscribed
- Order of notification not guaranteed
- Debugging can be challenging as execution flow is less obvious
The Observer pattern is your best friend whenever you have a one-to-many relationship where one object’s state change needs to trigger reactions in multiple other objects.