This lesson settles the oldest debt in the course. In the first lesson you saw an Order.changeStatus(...) that, besides changing the status, created a PushService, an SmsService, and a StatsPanel with new and called them one by one. We said back then: "there is a classic structure that solves exactly this... and it has a name". Three modules later, here it is. Observer inverts the relationship: the order stops knowing its interested parties, and it's the interested parties who subscribe to the order. One changes, many find out, nobody knows anybody. It is probably the most influential behavioral pattern in the catalog — from it descend the listeners of every UI, the events of every framework, and, ultimately, the event architectures we'll see in module 6.
Contents
- The debt from lesson 01-01, reread with module-4 eyes
- Intent and structure of the pattern
- Complete Java implementation
- Push vs. pull: how much to tell in the notification
- Details that bite: ordering, errors, and listener leaks
- Observer in the JDK and in UIs
- From Observer to events and pub-sub (the door to module 6)
- When to use it and when not to
- Relationship with other patterns
- Common mistakes
- Exercises and conclusion
The debt from lesson 01-01, reread with module-4 eyes
The scene of the crime, as we left it in the first lesson:
public void changeStatus(String newStatus) {
this.status = newStatus;
PushService push = new PushService();
push.send(this.getCustomer().getDeviceToken(), "Your order is: " + newStatus);
SmsService sms = new SmsService();
sms.send(this.getCourier().getPhone(), "Order " + this.getId() + ": " + newStatus);
StatsPanel panel = new StatsPanel();
panel.recordStatusChange(this.getId(), newStatus);
}Back then it just smelled bad; now you can name every offense with what you've learned:
- The hard-wired
news: the creational problem module 2 dismantled — impossible to test without sending real SMS. Orderknows the details of push, SMS, and statistics: a violation of DIP and SRP at once.- Every new interested party (email, loyalty, kitchen) modifies
Order: a head-on violation of OCP. - The number of interested parties is variable and growing — the very definition of the context this pattern calls for: a business object whose changes matter to a variable number of third parties it should not know.
The inversion that fixes it: Order doesn't call its interested parties; it keeps a list of anonymous subscribers and, when it changes, walks the list notifying through a minimal interface. Who is on the list is decided outside, in configuration — and can change at runtime.
Intent and structure of the pattern
Intent (GoF): define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
classDiagram
class OrderObserver {
<<interface>>
+orderUpdated(order: Order, previous: OrderState)
}
class Order {
-observers: List~OrderObserver~
-status: OrderState
+subscribe(o: OrderObserver)
+unsubscribe(o: OrderObserver)
+changeStatus(next: OrderState)
-notifyObservers(previous: OrderState)
}
class CustomerNotifier {
-notifier: Notifier
+orderUpdated(order, previous)
}
class CourierNotifier {
+orderUpdated(order, previous)
}
class KitchenMonitor {
+orderUpdated(order, previous)
}
class StatsPanel {
+orderUpdated(order, previous)
}
OrderObserver <|.. CustomerNotifier
OrderObserver <|.. CourierNotifier
OrderObserver <|.. KitchenMonitor
OrderObserver <|.. StatsPanel
Order o-- OrderObserver : notifies
| GoF role | In PideYa |
|---|---|
| Subject (keeps subscribers and notifies) | Order |
| Observer (minimal notification interface) | OrderObserver |
| ConcreteObserver | CustomerNotifier, CourierNotifier, KitchenMonitor, StatsPanel |
The essence is in the direction of the arrows: Order depends only on the OrderObserver interface — never on a concrete observer. The concrete ones depend on Order (they read its data), but Order knows neither how many there are nor what they do. The one-to-many dependency exists at runtime (the list), not at compile time.
sequenceDiagram
participant F as CheckoutFacade
participant P as Order (Subject)
participant NC as CustomerNotifier
participant NR as CourierNotifier
participant PE as StatsPanel
F->>P: changeStatus(OUT_FOR_DELIVERY)
P->>P: status = OUT_FOR_DELIVERY
P->>NC: orderUpdated(this, PAID)
NC->>NC: push to the customer
P->>NR: orderUpdated(this, PAID)
NR->>NR: SMS to the courier
P->>PE: orderUpdated(this, PAID)
PE->>PE: record metric
Note over P: Order doesn't know what each one did
Complete Java implementation
The Observer interface. One method, well named after the event. We pass the order and the previous status — the push/pull choice is discussed in the next section:
The Subject. The redeemed changeStatus, three modules later:
public class Order {
private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();
private OrderState status = OrderState.CREATED;
public void subscribe(OrderObserver observer) {
observers.add(Objects.requireNonNull(observer));
}
public void unsubscribe(OrderObserver observer) {
observers.remove(observer);
}
public void changeStatus(OrderState next) {
if (next == this.status) {
return; // no real change, no noise
}
OrderState previous = this.status;
this.status = next; // 1st consolidate the state...
notifyObservers(previous); // ...2nd notify
}
private void notifyObservers(OrderState previous) {
for (OrderObserver o : observers) {
try {
o.orderUpdated(this, previous);
} catch (RuntimeException e) {
// One broken observer must not take down the others' notification
EventLog.INSTANCE.error("Observer failed: " + o.getClass(), e);
}
}
}
}Annotated decisions, all with a scar behind them:
CopyOnWriteArrayList: lets an observer unsubscribe during a notification (or another thread subscribe) without aConcurrentModificationException. For subscriber lists — many reads, few writes — it's the ideal structure.- State first, notification after: observers will read
order.getStatus(); if you notify before consolidating, they read the past. try/catchper observer: without it, the first observer that throws leaves the rest without their notification — and a failure in the stats panel must not prevent the courier's SMS.- The no-change filter: notifying "changes" that change nothing produces cascades of noise (and, in observer graphs, loops).
The concrete observers. Each one a small class with a single reason to change — and reusing pieces from the course: the Notifier from Factory Method, perhaps wrapped in the RetryNotifier from Decorator:
public class CustomerNotifier implements OrderObserver {
private final Notifier notifier; // injected: push, sms, email... doesn't matter
public CustomerNotifier(Notifier notifier) {
this.notifier = notifier;
}
@Override
public void orderUpdated(Order order, OrderState previous) {
notifier.send("Your order " + order.getId() + " is: " + order.getStatus());
}
}
public class StatsPanel implements OrderObserver {
@Override
public void orderUpdated(Order order, OrderState previous) {
metrics.recordTransition(previous, order.getStatus());
}
}The assembly, in configuration (the CheckoutFacade when creating the order, or app startup):
order.subscribe(new CustomerNotifier(notifierRegistry.create("push")));
order.subscribe(new CourierNotifier(notifierRegistry.create("sms")));
order.subscribe(new KitchenMonitor(kitchenScreen));
order.subscribe(new StatsPanel(metrics));Review the offenses from 01-01: the service news are out of Order (and testing means subscribing a fake observer); the push/SMS details live in their observers; the new interested party — loyalty that adds points at DELIVERED — is one new class and one subscription line: Order stays untouched. Debt settled.
Push vs. pull: how much to tell in the notification
The notification method's signature admits a spectrum:
| Model | Typical signature | Advantage | Drawback |
|---|---|---|---|
| Pure push | orderUpdated(id, newStatus, eta, zone...) |
The observer doesn't touch the subject; may not even know it | The subject guesses what each one needs; signatures keep growing |
| Pure pull | orderUpdated() |
Minimal, stable signature | Everyone goes back to the subject to ask; the state may have changed again between notice and query |
| Hybrid | orderUpdated(order, previousStatus) |
A reference to query + the event's key datum | — |
We chose the hybrid for a reason: the previousStatus only exists at the moment of the notification (afterwards, nobody remembers it — the stats panel needs it to record transitions), and the reference to the order lets each observer read whatever it cares about without fattening the signature. Practical rule: push what's ephemeral about the event, leave what's queryable to pull.
Details that bite: ordering, errors, and listener leaks
Notification order. Our subject notifies in subscription order, but that's an implementation detail: Observer's contract is "everyone finds out", not "in this order". The serious mistake is designing observers that depend on the order (the SMS assumes stats already recorded): those are hidden dependencies among supposed strangers, invisible in the assembly code. If order truly matters, Observer is the wrong pattern — you want explicit orchestration (a Facade calling steps in order, or a Mediator directing).
Errors. Already seen in the code: isolate each observer with try/catch. The design question is what else to do — log and continue (our choice), retry (the module-3 retry decorator does it for us in the notifiers), or unsubscribe the repeat offender.
Listener leaks. The pattern's most famous production bug: the subject holds a strong reference to each observer, so an observer that forgets to unsubscribe cannot be garbage-collected while the subject lives — and it keeps receiving (and processing) notifications for a screen the user closed an hour ago. In PideYa: the tracking screen subscribes to the order; the user navigates back; the "dead" screen stays hanging off the order until it's delivered. Antidotes: unsubscribe symmetrically in the lifecycle (when the screen closes), and for short-lived subscribers on long-lived subjects, weak references (WeakReference) so the collector can take them.
Observer in the JDK and in UIs
The pattern permeates the platform:
- UI listeners (Swing's
ActionListener, Android/JavaFX listeners, the DOM'saddEventListener): every button is a subject; everyonClick, an observer. All interface programming is industrialized Observer. PropertyChangeListener/PropertyChangeSupport(java.beans): a reusable subject by composition — your class delegates the list and the notification toPropertyChangeSupportinstead of reimplementing them.java.util.Observable/Observer: the JDK's original support, deprecated since Java 9 — a class instead of an interface (it stole your inheritance), no generics, no guaranteed order. Its posthumous lesson: better a domain-specific interface of your own (ourOrderObserver) than a one-size-fits-all generic observer.java.util.concurrent.Flow(Java 9+): the modern version with backpressure (the subscriber requests how many elements it can process), the foundation of reactive programming (RxJava, Reactor).
From Observer to events and pub-sub (the door to module 6)
A natural generalization, only sketched: if instead of subscribing to one specific order the interested parties subscribe to event types (OrderDelivered, OrderCancelled) on an intermediary — an event bus — subject and observers stop knowing each other even as interfaces: you publish an event and whoever is subscribed to the type receives it. That is publish-subscribe (pub-sub); at process scale and with a broker (Kafka, RabbitMQ) in between, it is the foundation of the event-driven architectures with which PideYa will integrate kitchen, delivery, and billing in module 6. That entire world is this pattern with the subscriber list externalized. We leave it here: mention made, development there.
When to use it and when not to
Use it when:
- A change in one object matters to others whose number and identity vary (in configuration or at runtime).
- The observed object must not know its interested parties (independent modules, layers that shouldn't look at each other).
- You want to add new reactions without touching the one that changes (OCP).
Avoid it when:
- There is one interested party, fixed and forever: a direct call is clearer and easier to debug.
- The reactions must happen in guaranteed order or in a transaction with the change (all or nothing): unordered broadcast without acknowledgment doesn't give those guarantees; orchestrate explicitly.
- The cause-effect chain is already hard to follow: Observer's big cost is that the flow becomes invisible — looking at
changeStatus, nobody knows what will happen; you have to trace the subscriptions. In systems saturated with observers, debugging is archaeology. Good subscription/notification logging (viaEventLog) is oxygen.
Relationship with other patterns
- Mediator: the module's star matchup (face to face in the comparison): Observer broadcasts with no central protocol; Mediator directs with a protocol. And they combine: a mediator can learn about things by observing its colleagues.
- State (next lesson): direct partners — State will decide which transitions are legal on the order; Observer broadcasts the ones that happen.
- Singleton: global subjects (an event bus as a singleton) inherit all the global-state ills from module 2.
- Decorator: the notifier observers reuse the technical decorators (retries, logging) without the subject knowing.
- Command: notifications can materialize as queued commands — the first step toward asynchronous notification.
Common mistakes
- Forgetting to unsubscribe: the listener leak already described; subscribe/unsubscribe symmetry in the lifecycle, always.
- Heavy work on the notification thread: our loop is synchronous — an observer that takes 3 seconds freezes
changeStatus(and in a UI, the whole interface). Slow work goes to a queue or an executor (the notification queued as a command). - Observers that mutate the subject during the notification: an
orderUpdatedthat callsorder.changeStatus(...)triggers reentrant notifications and loops. If a reaction must cause another transition, let it request it afterwards (by queueing), not inside the notification. - Depending on the notification order among observers: hidden dependencies; already explained.
- Notifying while holding the lock (in synchronized subjects): a deadlock recipe if an observer reenters the subject. Copy the list and notify outside the critical section (the
CopyOnWriteArrayListgives you this for free). - Swallowing exceptions without logging them: isolating is not silencing; a
catchwithout a log turns real failures into mysteries.
Exercises
Exercise 1: the loyalty observer
Implement LoyaltyProgram implements OrderObserver: when an order reaches DELIVERED, add 1 point per euro of the total to the customer (pointsService.add(customerId, points)). The other statuses don't interest it. What did you have to touch in Order?
Exercise 2: hunt the leak
The TrackingScreen subscribes to the order in its constructor. Users complain that the app gets slower and slower during long orders, and the profiler shows hundreds of live TrackingScreen instances. Explain the exact cause (who retains whom?) and write the fix.
Exercise 3: Observer or direct call?
Decide and justify: (a) when the order is confirmed, charge through the payment gateway; (b) when the status changes, refresh the kitchen screen; (c) upon delivery, generate the invoice, which legally must exist before the order is considered closed; (d) when a dish's price changes, invalidate the CatalogCacheProxy from module 3.
Solutions
Solution 1:
public class LoyaltyProgram implements OrderObserver {
private final PointsService pointsService;
public LoyaltyProgram(PointsService pointsService) {
this.pointsService = pointsService;
}
@Override
public void orderUpdated(Order order, OrderState previous) {
if (order.getStatus() != OrderState.DELIVERED) {
return; // filters: only delivery matters to it
}
int points = order.getTotal().intValue(); // 1 point per euro
pointsService.add(order.getCustomer().getId(), points);
}
}In Order: nothing. One new class and one subscription line in the assembly. Compare it with the 01-01 version, where this would have been the fourth hard-wired block inside changeStatus — this exercise is the proof of the settled debt.
Solution 2: the order (a subject alive for hours) holds a strong reference to each TrackingScreen in its observer list; the user opens and closes the screen twenty times, and no instance can be collected because the order retains them — and all twenty keep processing every notification. Fix: symmetric unsubscription in the screen's lifecycle:
public class TrackingScreen implements OrderObserver {
private final Order order;
public TrackingScreen(Order order) {
this.order = order;
order.subscribe(this); // on open
}
public void onClose() { // UI lifecycle hook
order.unsubscribe(this); // mandatory symmetry
}
// ...
}(An extra defense if the lifecycle isn't reliable: have the subject store WeakReference<OrderObserver> and purge the dead ones when notifying.)
Solution 3: (a) direct call (orchestrated by the CheckoutFacade): the charge is an essential, synchronous step with error handling that belongs to the flow — not an "optional reaction"; (b) Observer: the kitchen is one interested party among several, with no ordering or transaction — a textbook KitchenMonitor; (c) direct call/orchestration: there's an ordering and atomicity guarantee ("before closing") that Observer doesn't offer by contract; (d) Observer: the cache is a technical interested party that should neither know the business catalog nor be known — and tomorrow the search engine and the price history may join (the variable interested parties of the definition).
Conclusion
The debt from the first lesson is settled, with interest: Order.changeStatus(...) no longer creates or knows anyone — it keeps a list of anonymous OrderObservers, consolidates its state, and broadcasts the notification while isolating failures; customer, courier, kitchen, statistics, and the newly arrived loyalty program each react in their own class, plugged and unplugged in configuration. You also take away the craft that separates the diagram from the pattern in production: hybrid push/pull with the previous status, not depending on order, isolating exceptions, and hunting the listener leak. And a door left ajar: externalize the subscriber list to a bus and you have pub-sub — module 6 awaits.
But notice what Observer does not watch: today nothing prevents changeStatus(DELIVERED) on a freshly created, unpaid order — we would dutifully notify everyone about an outrage. Which transitions are legal, and how the order's behavior changes depending on where it is in its life cycle — what can be cancelled, what can be modified — is a different problem: behavior that changes with internal state. See you in State.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
