Hook: Interviewers love this pattern because it tests whether you can keep objects loosely coupled while still reacting to change fast.
Question: What is the Observer Pattern?
Answer: The Observer Pattern is a way for one object, called the subject or publisher, to keep a list of dependent objects, called observers or subscribers, and notify them automatically when its state changes. It is useful when one change must update many things, like screens, alerts, or caches, without hard-wiring those classes together. In Java, you usually implement it with your own interfaces; the old java.util.Observable and Observer types are legacy and deprecated.
Interview-Ready Answer: I’d use the Observer pattern when one object changes and several others need to react immediately. My mental model is a newspaper subscription: the newspaper is the subject, and each subscriber gets the new issue automatically. In Java, I’d usually define my own observer interface and keep the subscriber list in the subject; if I have lots of reads and few subscription changes, I like CopyOnWriteArrayList because notifications stay safe even when an observer unsubscribes during an update.
The Observer Pattern is a behavioral design pattern, which means it focuses on how objects communicate. The main idea is simple: one object changes, and a set of interested objects are told about it automatically. This keeps the subject from knowing details about every listener, which makes the code easier to extend.
addObserver(). This step creates the subscription.update().removeObserver(). This matters because the subject still holds a reference to it, which can otherwise become a memory leak.There are two common callback styles. In a push model, the subject sends the new data directly. In a pull model, the subject sends an event or reference, and the observer asks for more details if needed. Push is simpler for small updates; pull is useful when observers need different parts of the state.
Use it when you want loose coupling means the subject and observers depend on small interfaces, not concrete classes. That makes the system easier to add to later, because you can attach a new observer without editing the subject’s core logic.
| Aspect | Observer | Polling | Pub-Sub |
|---|---|---|---|
| Update style | Direct callback | Periodic checks | Brokered event |
| Coupling | Medium-low | Low | Very low |
| Latency | Immediate | Interval-based | Immediate |
| Extra infra | None | Timer loop | Broker/queue |
| Best for | Local state change | Simple checks | Large distributed systems |
Think of Observer as a direct subscription between the subject and listeners. Pub-Sub usually adds a middle layer, such as a broker or event bus, which makes it better for distributed systems. Polling is simpler but wastes time because you keep checking even when nothing changed.
Notification is usually O(n) time because the subject must visit every observer. Registering and removing are often also O(n) if you use a list, because the structure may need to search or copy elements. Space is O(n) for the observer references. In real terms, if you have 1,000 observers and each one spends 2 milliseconds handling an event, a synchronous notification can take about 2 seconds total, which is too slow for a hot path like checkout or login.
A common Java choice is CopyOnWriteArrayList, which copies the underlying array on every write. That sounds expensive, but it is great when reads and notifications are common and subscription changes are rare. If observers are added and removed all the time, a different structure may be better. Also remember to catch failures per observer, otherwise one broken listener can prevent the rest from receiving the update.
One more gotcha: the pattern does not guarantee ordering unless your implementation does. If order matters, define it explicitly. Also be careful with re-entrant updates, where an observer changes the subject again inside update(); that can create loops or surprising cascades.
Real-World Example: Imagine an e-commerce checkout service. The OrderService is the subject, and observers include email receipts, SMS alerts, inventory updates, and analytics tracking. When an order is placed, the service notifies all subscribers so they can react without the checkout code knowing their details.
What goes wrong when teams misunderstand the pattern? A common outage happens when one observer does a slow network call inside the notification path. During a flash sale, the checkout thread waits on that slow listener, request latency spikes, the thread pool fills up, and users see spinning loaders or payment retries. Logs often show timeouts in the listener, not in the main checkout logic, which makes the bug hard to spot unless you know the notification chain.
The fix is usually to keep observers lightweight, catch exceptions per observer, and move slow work to an async queue or background worker. The pattern itself is fine; the mistake is treating every observer like a tiny, fast callback when one of them really behaves like a remote service call.
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
public class ObserverPatternDemo {
interface Observer {
void update(String headline, NewsAgency agency);
}
static class NewsAgency {
private final CopyOnWriteArrayList<Observer> observers = new CopyOnWriteArrayList<>();
private String headline = "";
public void addObserver(Observer observer) {
Objects.requireNonNull(observer, "observer");
// Avoid duplicate subscriptions. Otherwise the same listener would fire twice.
if (!observers.contains(observer)) {
observers.add(observer);
}
}
public void removeObserver(Observer observer) {
observers.remove(observer); // Safe no-op if it was already removed.
}
public void setHeadline(String headline) {
this.headline = Objects.requireNonNull(headline, "headline");
notifyObservers();
}
public String getHeadline() {
return headline;
}
private void notifyObservers() {
// CopyOnWriteArrayList lets observers unsubscribe during notification
// without ConcurrentModificationException.
for (Observer observer : observers) {
try {
observer.update(headline, this);
} catch (RuntimeException ex) {
// One bad listener should not stop the others from receiving the update.
System.out.println("Observer failed: "
+ observer.getClass().getSimpleName()
+ " -> " + ex.getMessage());
}
}
}
}
static class EmailSubscriber implements Observer {
private final String name;
EmailSubscriber(String name) {
this.name = name;
}
@Override
public void update(String headline, NewsAgency agency) {
System.out.println(name + " received email: " + headline);
}
}
static class SmsSubscriber implements Observer {
private final String name;
SmsSubscriber(String name) {
this.name = name;
}
@Override
public void update(String headline, NewsAgency agency) {
System.out.println(name + " received SMS: " + headline);
}
}
static class AutoUnsubscribingSubscriber implements Observer {
private final String name;
private boolean unsubscribed = false;
AutoUnsubscribingSubscriber(String name) {
this.name = name;
}
@Override
public void update(String headline, NewsAgency agency) {
System.out.println(name + " saw: " + headline);
// This is the edge case: the observer removes itself while notifications are in flight.
if (!unsubscribed && headline.toLowerCase(Locale.ROOT).contains("breaking")) {
unsubscribed = true;
System.out.println(name + " unsubscribes after breaking news.");
agency.removeObserver(this);
}
}
}
static class FaultySubscriber implements Observer {
private final String name;
FaultySubscriber(String name) {
this.name = name;
}
@Override
public void update(String headline, NewsAgency agency) {
throw new IllegalStateException(name + " network timeout");
}
}
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
Observer email = new EmailSubscriber("Email");
Observer sms = new SmsSubscriber("SMS");
Observer oneShot = new AutoUnsubscribingSubscriber("OneShot");
Observer faulty = new FaultySubscriber("FraudMonitor");
agency.addObserver(email);
agency.addObserver(sms);
agency.addObserver(oneShot);
agency.addObserver(faulty);
agency.addObserver(email); // Duplicate subscription is ignored.
System.out.println("----- first headline -----");
agency.setHeadline("Daily update: market opens higher");
System.out.println("----- second headline -----");
agency.setHeadline("Breaking: payment outage resolved");
// Removing twice should be harmless.
agency.removeObserver(sms);
agency.removeObserver(sms);
System.out.println("----- final headline -----");
agency.setHeadline("Final update: all systems green");
System.out.println("Current stored headline: " + agency.getHeadline());
}
}
Follow-up & Tricky Questions:
CopyOnWriteArrayList is a strong choice because iteration is safe and simple. If membership changes are frequent, you may prefer a concurrent set or another design.java.util.Observable the recommended Java API? No. It is legacy and deprecated, so in modern Java you normally build your own observer abstraction or use a newer event/reactive API.update()? Yes, but that can create re-entrant updates or infinite loops if you are not careful. It is usually safer to keep callbacks small and side-effect aware.Common Mistakes:
Observable and Observer are old. Fix: prefer your own interface or a modern event/reactive approach.Memory Hook: Think newspaper subscription: the subject is the paper, observers are subscribers, and every new issue is delivered to everyone on the mailing list.
Cheat Sheet:
CopyOnWriteArrayList is good when reads dominate writes.Practice Tasks: