Observer: Subscriptions and State-Change Notifications
Starting from a scenario where one object's state change needs to notify multiple interested parties, see how the Observer Pattern decouples publishers from subscribers — and how it relates to the browser's native event system and Pub/Sub.
Start With a YouTube Subscription
If you've ever subscribed to a YouTube channel, this pattern will feel familiar: when the channel uploads a new video, it doesn't need to know who's subscribed or how many people are watching — it just publishes the video. Each subscriber independently decides whether to subscribe and whether to receive notifications. Beyond that subscription relationship, the channel and its subscribers don't know anything about each other.
The Observer Pattern brings this idea into code: the object being watched is called the Subject, and the objects watching it are called Observers. The Subject keeps a list of Observers and notifies each one when its state changes, without ever needing to know what any individual Observer actually does with that notification.
Building a Basic Observer Pattern From Scratch
Let's implement it with the classic stock price example:
class StockSubject {
#observers = []
#price = 0
subscribe(observer) {
this.#observers.push(observer)
}
unsubscribe(observer) {
this.#observers = this.#observers.filter((o) => o !== observer)
}
setPrice(price) {
this.#price = price
this.#notify()
}
#notify() {
for (const observer of this.#observers) {
observer.update(this.#price)
}
}
}
class PriceChart {
update(price) {
console.log(`[Chart] Price updated: ${price}`)
}
}
class PriceAlert {
update(price) {
if (price > 100) {
console.log(`[Alert] Price crossed 100: ${price}`)
}
}
}Here's how it's used in practice:
const stock = new StockSubject()
const chart = new PriceChart()
const alert = new PriceAlert()
stock.subscribe(chart)
stock.subscribe(alert)
stock.setPrice(95)
// [Chart] Price updated: 95
stock.setPrice(105)
// [Chart] Price updated: 105
// [Alert] Price crossed 100: 105StockSubject never knows what PriceChart or PriceAlert do internally — its only job is broadcasting the latest price to every subscriber whenever setPrice() is called. Want to add a feature that logs price history? Just write another class that implements update() and call subscribe() on it. StockSubject itself doesn't need to change at all.
Don't Forget to Unsubscribe, or You'll Grow a Memory Leak
There's a detail that's easy to overlook with the Observer Pattern: if you subscribe and forget to unsubscribe, the Subject's Observer list keeps holding a reference to that Observer forever. Even once the Observer is no longer needed anywhere else, garbage collection can't reclaim it — someone's still holding its hand.
const shortLivedChart = new PriceChart()
stock.subscribe(shortLivedChart)
// This component isn't needed anymore, but if you forget to unsubscribe,
// stock's internal #observers array keeps holding a reference to it, and it never gets collected
stock.unsubscribe(shortLivedChart) // this step isn't optionalThis shows up constantly in frontend frameworks. If a component forgets to clean up its subscriptions on unmount, you get a memory leak — and worse, a component that no longer exists on screen keeps silently handling events and consuming resources.
JavaScript Already Has the Observer Pattern Built In
You're actually using the Observer Pattern every day — it's just wearing a different name. The browser's addEventListener is this pattern at its core:
const button = document.querySelector('button')
function handleClick() {
console.log('Button clicked')
}
button.addEventListener('click', handleClick) // subscribe
button.removeEventListener('click', handleClick) // unsubscribebutton is the Subject, handleClick is the Observer, addEventListener maps to subscribe(), and removeEventListener maps to unsubscribe(). Node.js's EventEmitter follows the exact same design:
import { EventEmitter } from 'node:events'
const emitter = new EventEmitter()
emitter.on('priceChanged', (price) => {
console.log(`Price changed: ${price}`)
})
emitter.emit('priceChanged', 105) // Price changed: 105Once you understand the core idea behind the Observer Pattern, you start noticing that these seemingly built-in framework features are all just different implementations of the same mechanism.
Are Observer and Pub/Sub the Same Thing?
These two terms get used interchangeably a lot, but strictly speaking, they differ in how coupled the two sides are:
- Observer Pattern: the Subject holds a direct reference to each Observer — both sides know the other exists. It's a fairly direct one-to-many relationship.
- Publish/Subscribe (Pub/Sub): there's an extra layer between the Publisher and Subscriber — an event channel or message broker. Neither side knows about the other; both only talk to that intermediary.
class EventBus {
#listeners = {}
on(eventName, callback) {
if (!this.#listeners[eventName]) {
this.#listeners[eventName] = []
}
this.#listeners[eventName].push(callback)
}
emit(eventName, payload) {
const callbacks = this.#listeners[eventName] || []
callbacks.forEach((callback) => callback(payload))
}
}
const eventBus = new EventBus()
// the publisher doesn't need to know who's subscribed
eventBus.on('order:placed', (order) => {
console.log(`Sending confirmation email for order ${order.id}`)
})
eventBus.emit('order:placed', { id: 'order_01' })
// Sending confirmation email for order order_01In this example, whatever triggers emit('order:placed', ...) doesn't need to know what listeners are hanging off EventBus — the two sides communicate indirectly through an event name string. That looser coupling is exactly why Pub/Sub gets used for cross-module and even cross-service communication. You could think of the Observer Pattern as a more direct, lighter-weight special case of Pub/Sub.
The Trade-offs: Too Many Layers of Notification Gets Messy
The Observer Pattern solves the coupling problem between a state owner and the parties watching it, but it introduces a few things worth watching out for:
- Notification order isn't guaranteed to be intuitive. If Observers depend on each other (say, B needs A to finish first), a plain loop over the list can't guarantee that ordering, which makes it easy to end up with hidden execution-order dependencies.
- Chained updates are hard to trace. If one Observer's
update()changes another Subject's state and triggers a whole new round of notifications, these can stack up, and debugging becomes a nightmare of figuring out who actually triggered a given update. - Leaks from forgotten unsubscribes. The memory leak issue mentioned earlier is especially dangerous in long-running applications.
This is why libraries like Vue's reactivity system and RxJS build on top of the Observer Pattern and additionally handle dependency tracking, batching, and automatic unsubscription — so developers don't have to manage these error-prone details by hand.
Wrapping Up
The core value of the Observer Pattern is that a state owner doesn't need to know every party that's interested in it — it just broadcasts a notification when its state changes, and leaves the actual handling logic entirely up to the subscribers. The browser's addEventListener and Node.js's EventEmitter are, at their core, both concrete implementations of this same pattern.