
Observer Pattern: The Secret Behind YouTube Subscribe
Don't check every second if a new video is up. One subscribe button automatically notifies you. The elegant design pattern solving 1:N dependencies.

Don't check every second if a new video is up. One subscribe button automatically notifies you. The elegant design pattern solving 1:N dependencies.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

When I first learned React, useState and useEffect seemed magical. State changes, and components automatically re-render.
"How does it know state changed and auto-update?"
A senior developer told me, "That's the Observer Pattern." So I decided to study it properly. But the real moment came when I was building real-time notifications for my service. Users shouldn't have to refresh the page every time. When the server has updates, the screen should automatically update. That's when I realized I needed the Observer Pattern.
"Subject changes, Observers get notified"
Too abstract for me.
What made it more confusing was that I was already using addEventListener. I didn't know it was the Observer Pattern. I just thought "that's how event listeners work." I assumed patterns were something more complex, but here I was using one every day without knowing it.
At first, I thought I could just use a loop to check state changes.
// I didn't understand why this doesn't work
while (true) {
if (dataChanged) {
updateUI();
}
}
Running this code freezes the browser. Why? Because the infinite loop blocks the main thread. That's when I realized: "Actively checking" vs "Passively receiving notifications" are completely different approaches.
Then I heard the YouTube subscription analogy and it clicked completely.
// Check every second
setInterval(() => {
if (hasNewVideo()) {
console.log('New video!');
}
}, 1000);
Keep checking if a video is up. Even when there's nothing, keep asking. Huge waste.
In my early service days, I built it this way. Checking for user notifications every second by hitting the server. Once we crossed 100 users, the server started lagging. 100 requests per second, and 99 of them returned "no notifications." Just wasting CPU and network bandwidth.
// Subscribe (once)
youtuber.subscribe(me);
// YouTuber uploads → auto-notify subscribers
youtuber.uploadVideo('New video!');
// → me.notify('New video!') auto-called
Notifications only when new videos are up. No need to keep checking.
This analogy made me go "Ah, that's why it's called Observer!" That was it. A system that only reaches out when needed. Instead of constantly asking "anything new?", you register once and say "let me know when there is."
Now that I properly understand it, the Observer Pattern consists of two roles:
subscribe and unsubscribenotify() when state changesupdate() or notify() methodHere's how I understand it: Subject is the "broadcasting station", Observers are "viewers". The station only knows how many viewers it has, not what each viewer does with the content. Viewers don't need to know how the station produces content. They just exchange one signal: "broadcast started" and that's it.
class YouTuber {
constructor() {
this.subscribers = []; // List of observers
}
subscribe(observer) {
this.subscribers.push(observer);
console.log('Subscriber added!');
}
unsubscribe(observer) {
this.subscribers = this.subscribers.filter(sub => sub !== observer);
console.log('Unsubscribed');
}
uploadVideo(title) {
console.log(`[YouTuber] New upload: ${title}`);
// Notify all subscribers
this.notifyAll(title);
}
notifyAll(data) {
this.subscribers.forEach(observer => {
observer.update(data);
});
}
}
class Subscriber {
constructor(name) {
this.name = name;
}
update(videoTitle) {
console.log(`[${this.name}] Notification: "${videoTitle}"`);
}
}
const youtuber = new YouTuber();
const alice = new Subscriber('Alice');
const bob = new Subscriber('Bob');
const charlie = new Subscriber('Charlie');
// Subscribe
youtuber.subscribe(alice);
youtuber.subscribe(bob);
youtuber.subscribe(charlie);
// Upload video
youtuber.uploadVideo('Observer Pattern Explained');
// Output:
// [YouTuber] New upload: Observer Pattern Explained
// [Alice] Notification: "Observer Pattern Explained"
// [Bob] Notification: "Observer Pattern Explained"
// [Charlie] Notification: "Observer Pattern Explained"
// Unsubscribe
youtuber.unsubscribe(bob);
youtuber.uploadVideo('Second Video');
// Bob doesn't get notified
When I first wrote this code, it felt magical. Just calling notifyAll once and it automatically propagates to all subscribers. What I learned was a simple truth: "It's just iterating through a list and calling each method."
Node.js has the Observer Pattern built-in. It's called EventEmitter.
const EventEmitter = require('events');
class YouTuber extends EventEmitter {
uploadVideo(title) {
console.log(`[YouTuber] New upload: ${title}`);
this.emit('newVideo', title); // Emit event
}
}
const youtuber = new YouTuber();
// Subscriber 1
youtuber.on('newVideo', (title) => {
console.log(`[Alice] Notification: ${title}`);
});
// Subscriber 2
youtuber.on('newVideo', (title) => {
console.log(`[Bob] Notification: ${title}`);
});
youtuber.uploadVideo('Observer Pattern in Practice');
// Output:
// [YouTuber] New upload: Observer Pattern in Practice
// [Alice] Notification: Observer Pattern in Practice
// [Bob] Notification: Observer Pattern in Practice
With EventEmitter, you don't need to manually implement subscribe, unsubscribe, or notify. Just use .on() to subscribe and .emit() to publish. The core principle is identical: "Register once, get notified when events happen."
I used this in production. When building a file upload progress feature, I made uploadManager extend EventEmitter.
class UploadManager extends EventEmitter {
upload(file) {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
this.emit('progress', progress); // Progress notification
if (progress >= 100) {
clearInterval(interval);
this.emit('complete', file.name); // Complete notification
}
}, 500);
}
}
const uploader = new UploadManager();
uploader.on('progress', (percent) => {
console.log(`Uploading... ${percent}%`);
});
uploader.on('complete', (fileName) => {
console.log(`${fileName} upload complete!`);
});
uploader.upload({ name: 'image.png' });
This cleanly separated the UI update logic. UploadManager only cares about uploading files, and progress display is handled outside.
At first, I thought Pub/Sub (Publish-Subscribe) and Observer Pattern were the same. Both involve "subscribe and get notified." But there's a subtle difference.
// Observer Pattern
youtuber.subscribe(alice); // youtuber directly manages alice
youtuber.uploadVideo('video');
// youtuber → alice.update() called directly
// Pub/Sub Pattern (Redis example)
publisher.publish('video-channel', 'New video');
// → Message Broker (Redis) → Delivers to Subscribers
subscriber.subscribe('video-channel', (msg) => {
console.log(msg);
});
When I understood this difference, I realized: Observer is "direct phone call", Pub/Sub is "radio broadcast". Observer needs to know the phone number, Pub/Sub only needs to know the channel.
In MSA architectures, Pub/Sub is commonly used for inter-service communication. The pattern is: the order service publishes an "order completed" event, and the payment service, shipping service, and notification service each subscribe and handle it independently. Studying this pattern made it clear why it beats Observer Pattern here — with Observer, the order service would need to know about all other services directly.
There's a library that takes the Observer Pattern to the extreme: RxJS (Reactive Extensions for JavaScript).
import { fromEvent } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';
// Convert search input events to Observable
const searchInput = document.querySelector('#search');
const search$ = fromEvent(searchInput, 'input');
// Chain observers in a pipeline
search$.pipe(
map(event => event.target.value), // Extract value
filter(text => text.length > 2), // Only 3+ chars
debounceTime(300) // Wait 300ms
).subscribe(searchTerm => {
console.log('Search:', searchTerm);
// Call API
});
When I first saw RxJS, it was overwhelming. "What's an Observable? What's an Operator?" But after understanding the Observer Pattern, it started making sense. Observable is the Subject, subscribe() registers an Observer, pipe() adds intermediate processing stages.
If you implement this with pure Observer Pattern:
// Without RxJS...
let timeout;
searchInput.addEventListener('input', (event) => {
const value = event.target.value;
if (value.length <= 2) return; // filter
clearTimeout(timeout);
timeout = setTimeout(() => { // debounceTime
console.log('Search:', value);
}, 300);
});
It works the same. But RxJS lets you write it declaratively and makes it easier to combine multiple streams. That's when I understood: "Observer Pattern = A way to handle data flows."
The most familiar example.
const button = document.querySelector('button');
// Subscribe (addEventListener)
button.addEventListener('click', () => {
console.log('Button clicked!');
});
// Subject: button
// Observer: callback function
// Event occurs (click) → notify all listeners
The code I used every day was the Observer Pattern. I finally get it.
function Counter() {
const [count, setCount] = useState(0);
// Auto re-render when count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
count stateCounter componentCounterIf you look inside React, useState registers the component as an Observer. When state changes, it sends a "re-render please" signal to registered components.
// Store (Subject)
const store = createStore(reducer);
// Component (Observer)
store.subscribe(() => {
console.log('State changed:', store.getState());
});
// Dispatch (state change)
store.dispatch({ type: 'INCREMENT' });
// → Notify all subscribers
When I used Redux in my service, I didn't realize store.subscribe() was exactly the Observer Pattern. I just thought "this is how Redux works." After understanding the pattern, I saw why it was designed this way.
const socket = new WebSocket('ws://chat.server.com');
// Observable: WebSocket
// Observer: message handler
socket.addEventListener('message', (event) => {
console.log('New message:', event.data);
updateChatUI(event.data);
});
// Auto-notified when server sends message
When building my first real-time chat, I wondered "how do I receive messages when I don't know when the server will send them?" The Observer Pattern was the answer. Don't wait for the server to send, just register "notify me when you send" and you're done.
Subject and Observer don't need to know each other's specifics. Subject only needs to know "who subscribed", not the Observer's internal implementation.
// Subject doesn't care if Observer is Subscriber, Logger, or Analytics
youtuber.subscribe(new Subscriber('User'));
youtuber.subscribe(new Logger());
youtuber.subscribe(new Analytics());
I realized why this matters when I had a feature that started as "send notification on video upload", then later needed "log views" and "update recommendation algorithm". Thanks to the Observer Pattern, I didn't touch the YouTuber class at all, just added new Observers.
You can freely add/remove Observers at runtime.
// Subscribe/unsubscribe at runtime
if (userWantsNotification) {
subject.subscribe(observer);
} else {
subject.unsubscribe(observer);
}
This was useful when building a notification toggle feature. Users could turn notifications on/off in real-time without server restart.
One event can notify multiple Observers simultaneously. Elegantly handles 1:N relationships.
If you don't properly unsubscribe Observers, memory leaks occur.
// ❌ Bad
function badComponent() {
const observer = new Observer();
subject.subscribe(observer);
// No unsubscribe → memory leak!
}
// ✅ Good
function goodComponent() {
const observer = new Observer();
subject.subscribe(observer);
return () => {
subject.unsubscribe(observer); // Cleanup
};
}
Same in React:
useEffect(() => {
const handleResize = () => console.log('Resized');
window.addEventListener('resize', handleResize);
// Cleanup required!
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
I caused a memory leak once by not knowing this. Even after components unmounted, event listeners stayed alive, causing memory usage to grow over time. Only after finding it with Chrome DevTools Memory Profiler did I understand the importance of cleanup functions.
The order in which Observers are called isn't guaranteed.
subject.subscribe(observerA);
subject.subscribe(observerB);
subject.notify(); // A first? B first? Not guaranteed
If order matters, you need to manage it yourself. For example, if you need "logging always first", you'd need to implement a priority queue.
"Who called this function?" is hard to trace. The call stack shows notify() → observer.update(), but why notify was called isn't visible.
I had a bug where "why is the screen flickering twice?" Turns out two Observers were updating the same DOM, and it took forever to find where they subscribed.
// ❌ Infinite loop!
useEffect(() => {
setCount(count + 1); // Change count
}, [count]); // Re-run when count changes → infinite loop
Understanding the Observer Pattern makes it obvious why this loops infinitely:
When an Observer modifies the Subject, you get circular dependency. After learning this, I became very careful with useEffect dependency arrays.
Fix: Empty the dependency array or add conditions.
// ✅ Run once
useEffect(() => {
setCount(1);
}, []); // Empty array → runs once on mount
// WebSocket stays connected
useEffect(() => {
const ws = new WebSocket('ws://server.com');
ws.addEventListener('message', handleMessage);
// ❌ No cleanup → memory leak
});
Even after the component unmounted, the WebSocket stayed alive, wasting memory. Worse, when the server sent messages, it tried to update state on a dead component, causing errors.
Warning: Can't perform a React state update on an unmounted component.
When I first saw this error, I had no idea what it meant. "Why is it trying to update when it's unmounted?" After understanding the Observer Pattern, I realized "oh, the WebSocket is still registered as an Observer."
// ✅ Correct way
useEffect(() => {
const ws = new WebSocket('ws://server.com');
ws.addEventListener('message', handleMessage);
return () => {
ws.close(); // Cleanup
};
}, []);
Cleanup functions aren't optional, they're mandatory. If you don't make this a habit, you'll suffer from memory leaks eventually.
I made the search input call the API on every keystroke.
searchInput.addEventListener('input', (e) => {
fetch(`/api/search?q=${e.target.value}`);
});
Problem: every single character triggers a request. Typing "Observer Pattern" fires 7 requests. Almost crashed the server.
Fix: Added debounce to only request after typing stops.
let timeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fetch(`/api/search?q=${e.target.value}`);
}, 300); // Wait 300ms
});
The Observer Pattern's principle is "notify immediately on event". But in practice, getting notified too often is also a problem. You need additional techniques like Throttle and Debounce.
Pattern books only highlight benefits, but in practice, "when not to use it" is equally important.
// Observer Pattern (overkill)
class Button {
constructor() {
this.listeners = [];
}
subscribe(fn) { this.listeners.push(fn); }
click() { this.listeners.forEach(fn => fn()); }
}
// Just callback (sufficient)
function Button(onClick) {
this.onClick = onClick;
}
button.onClick();
If you only have 1 Observer, you don't need the pattern. Just accept a function and call it.
Observer Pattern is asynchronous. If order matters or you need return values, it's not suitable.
// ❌ Hard with Observer
const result = subject.notify(); // How do I get result?
// ✅ Direct call
const result = processData(data);
When I built payment logic, I initially made a "payment completed" event that multiple Observers would handle. But I needed guaranteed order: payment validation → inventory deduction → receipt generation. And if anything failed, rollback was needed. Observer Pattern made transaction management difficult. I ended up switching to direct function calls.
Observer Pattern makes call flow hard to trace. When rapidly prototyping and debugging, it can be a hindrance.
There's a temptation to make everything event-driven. "This looks cleaner!" But over-abstraction is poison. Three months later, when you look at your code, you'll waste time figuring out "where is this event triggered?"
For type safety and reusability, here's a generic TypeScript implementation:
interface Observer<T> {
update(data: T): void;
}
class Subject<T> {
private observers: Observer<T>[] = [];
subscribe(observer: Observer<T>): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer<T>): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data: T): void {
this.observers.forEach(observer => observer.update(data));
}
}
// Usage
interface VideoData {
title: string;
duration: number;
}
class VideoNotifier implements Observer<VideoData> {
constructor(private name: string) {}
update(data: VideoData): void {
console.log(`${this.name} notified: ${data.title} (${data.duration}s)`);
}
}
const youtubeChannel = new Subject<VideoData>();
const viewer1 = new VideoNotifier('Alice');
const viewer2 = new VideoNotifier('Bob');
youtubeChannel.subscribe(viewer1);
youtubeChannel.subscribe(viewer2);
youtubeChannel.notify({ title: 'TypeScript Tutorial', duration: 600 });
TypeScript generics ensure type safety. The compiler catches if you try to notify with the wrong data type.
What I learned from understanding the Observer Pattern:
React's useState, event listeners, WebSocket... they're all Observer Pattern. "Auto-updates" isn't magic anymore. It's a clear, understandable pattern.
That was it. "I don't know when I'll contact you, so leave your number and I'll call you." This one sentence captures the Observer Pattern for me.