Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Observer
Let an object announce events to any number of subscribers, without knowing who they are or what they do, so new reactions can be added without touching the source.
The problem
When an order is placed, the code sends a confirmation email, updates inventory, notifies the warehouse and records analytics. All of that lives inside place_order(). Every new reaction (loyalty points, a Slack alert for big orders) means editing the order code, and a failure in the analytics call can break order placement.
The order module now imports the email, inventory, warehouse and analytics modules. It knows far too much.
Real-world analogy
A newsletter. The publisher doesn't know its readers personally; it just sends each issue to everyone on the list. People subscribe and unsubscribe whenever they like, and the publisher's work doesn't change.
Without the pattern
def place_order(order):
save(order)
send_confirmation_email(order) # order code knows every reaction
reserve_inventory(order)
notify_warehouse(order)
track_analytics(order) # and needs editing for every new one
The pattern
- Subject (also called publisher or observable): keeps a list of subscribers, with
subscribe(),unsubscribe()andnotify(event). - Observers (subscribers or listeners): have one method, such as
update(event), or are plain callback functions. - When something happens, the subject calls every observer. It knows only that they have the update method, not what they do.
The code
class OrderEvents: # subject
def __init__(self):
self.subscribers = []
def subscribe(self, callback):
self.subscribers.append(callback)
return lambda: self.subscribers.remove(callback) # returns an unsubscribe function
def publish(self, order):
for callback in list(self.subscribers):
callback(order)
def send_email(order):
print(f" email: thanks for order #{order['id']}")
def reserve_stock(order):
print(f" inventory: {order['items']} item(s) reserved")
def big_order_alert(order):
if order["total"] >= 1000:
print(f" sales: big order #{order['id']}!")
events = OrderEvents()
events.subscribe(send_email)
events.subscribe(reserve_stock)
stop_alerts = events.subscribe(big_order_alert)
print("order 1 placed")
events.publish({"id": 1, "items": 3, "total": 1200})
stop_alerts() # observers can leave at any time
print("order 2 placed")
events.publish({"id": 2, "items": 1, "total": 1500})
Output
order 1 placed email: thanks for order #1 inventory: 3 item(s) reserved sales: big order #1! order 2 placed email: thanks for order #2 inventory: 1 item(s) reserved
type Order = { id: number; items: number; total: number };
type Listener = (order: Order) => void;
class OrderEvents { // subject
private subscribers: Listener[] = [];
subscribe(callback: Listener): () => void {
this.subscribers.push(callback);
return () => { this.subscribers = this.subscribers.filter((s) => s !== callback); }; // unsubscribe function
}
publish(order: Order) {
for (const callback of [...this.subscribers]) callback(order);
}
}
const sendEmail: Listener = (order) => console.log(` email: thanks for order #${order.id}`);
const reserveStock: Listener = (order) => console.log(` inventory: ${order.items} item(s) reserved`);
const bigOrderAlert: Listener = (order) => {
if (order.total >= 1000) console.log(` sales: big order #${order.id}!`);
};
const events = new OrderEvents();
events.subscribe(sendEmail);
events.subscribe(reserveStock);
const stopAlerts = events.subscribe(bigOrderAlert);
console.log("order 1 placed");
events.publish({ id: 1, items: 3, total: 1200 });
stopAlerts(); // observers can leave at any time
console.log("order 2 placed");
events.publish({ id: 2, items: 1, total: 1500 });
Output
order 1 placed email: thanks for order #1 inventory: 3 item(s) reserved sales: big order #1! order 2 placed email: thanks for order #2 inventory: 1 item(s) reserved
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
OrderEvents events = new OrderEvents();
events.subscribe(o -> System.out.println(" email: thanks for order #" + o.id()));
events.subscribe(o -> System.out.println(" inventory: " + o.items() + " item(s) reserved"));
Runnable stopAlerts = events.subscribe(o -> {
if (o.total() >= 1000) System.out.println(" sales: big order #" + o.id() + "!");
});
System.out.println("order 1 placed");
events.publish(new Order(1, 3, 1200));
stopAlerts.run(); // observers can leave at any time
System.out.println("order 2 placed");
events.publish(new Order(2, 1, 1500));
}
}
record Order(int id, int items, int total) {}
class OrderEvents { // subject
private final List<Consumer<Order>> subscribers = new ArrayList<>();
Runnable subscribe(Consumer<Order> callback) {
subscribers.add(callback);
return () -> subscribers.remove(callback); // returns an unsubscribe function
}
void publish(Order order) {
for (Consumer<Order> callback : List.copyOf(subscribers)) callback.accept(order);
}
}
Output
order 1 placed email: thanks for order #1 inventory: 3 item(s) reserved sales: big order #1! order 2 placed email: thanks for order #2 inventory: 1 item(s) reserved
Run this on your computer — see “Get Started” for setup.
<?php
class OrderEvents { // subject
private array $subscribers = [];
private int $nextId = 0;
public function subscribe(callable $callback): Closure {
$id = $this->nextId++;
$this->subscribers[$id] = $callback;
return function () use ($id) { unset($this->subscribers[$id]); }; // returns an unsubscribe function
}
public function publish(array $order): void {
foreach ($this->subscribers as $callback) $callback($order);
}
}
$events = new OrderEvents();
$events->subscribe(fn($o) => print(" email: thanks for order #{$o['id']}\n"));
$events->subscribe(fn($o) => print(" inventory: {$o['items']} item(s) reserved\n"));
$stopAlerts = $events->subscribe(function ($o) {
if ($o["total"] >= 1000) echo " sales: big order #{$o['id']}!\n";
});
echo "order 1 placed\n";
$events->publish(["id" => 1, "items" => 3, "total" => 1200]);
$stopAlerts(); // observers can leave at any time
echo "order 2 placed\n";
$events->publish(["id" => 2, "items" => 1, "total" => 1500]);
Output
order 1 placed email: thanks for order #1 inventory: 3 item(s) reserved sales: big order #1! order 2 placed email: thanks for order #2 inventory: 1 item(s) reserved
Order placement knows only that "something" listens. Loyalty points or a Slack alert would be one more subscribe() call in startup code.
When to use it
- Changes in one object should trigger reactions in others, and the set of reactions varies or grows.
- You want to decouple the source of an event from its consumers (UI updates, domain events, notifications).
- Several independent features care about the same event.
When not to use it
- There is exactly one reaction that always happens. A direct call is clearer and easier to trace.
- Order and failures matter. Observers usually run in an unspecified order, and one failing observer can stop the others or the subject. Decide how errors are handled.
- Watch for memory leaks: observers that never unsubscribe keep objects alive (a classic in UI code and long-lived event emitters).
- Cascades (observers that trigger more events) can become hard to follow. Across services, use a message queue or event bus (Pub/Sub, covered later) instead.
You've already used it
- DOM events:
addEventListener("click", ...). - Node.js
EventEmitter, RxJS observables, Vue and MobX reactivity, React'suseSyncExternalStoresubscriptions. - Laravel events and model observers; Django signals; Java's
PropertyChangeListenerand Spring'sApplicationEventlisteners.
Key points
- Observer lets a subject notify any number of subscribers without knowing what they do.
- Subscribers can join and leave at runtime; the subject never changes.
- Decide on ordering and error handling, and always unsubscribe to avoid leaks.
- Further reading: Refactoring Guru: Observer.
Practice questions
What problem does the Observer pattern solve?
Answer
It decouples the source of an event from the code that reacts to it. A subject keeps a list of subscribers and notifies them when something happens; new reactions are added by subscribing, without changing the subject.
What causes memory leaks with the Observer pattern?
Answer
The subject holds references to its observers. If an observer (a UI component, a request-scoped object) never unsubscribes, it can't be garbage collected, and it may keep reacting to events after it should be gone. Always unsubscribe, or use weak references where supported.
Name three framework features built on the Observer pattern.
Answer
DOM addEventListener, Node.js EventEmitter, RxJS observables, Laravel events and model observers, Django signals, and Spring application event listeners.
What should you decide about errors in observers?
Answer
Whether one failing observer should stop the others or the subject's operation. Often each observer is wrapped so failures are logged and don't break the action that published the event; critical reactions may instead need a durable queue with retries.
Open in the app to mark lessons complete, take notes and track your progress.