Design Patterns (Module / Observer) Get Certified
Design patterns are reusable solutions to common problems in software design. Two of the most useful in everyday JavaScript are the Module Pattern (for hiding implementation details) and the Observer Pattern (for letting different parts of an app react to events without being tightly coupled).
The Module Pattern
What this code does: Builds a counterModule with an IIFE and a closure. The count variable stays private. We increment twice, decrement once, then print the final count. Trying to access count directly from outside returns undefined.
const counterModule = (function () {
// Private state — not accessible from outside
let count = 0;
function increment() {
count++;
return count;
}
function decrement() {
count--;
return count;
}
function getCount() {
return count;
}
// Only these three are exposed publicly
return {
increment,
decrement,
getCount
};
})();
console.log("Start:", counterModule.getCount());
counterModule.increment();
counterModule.increment();
counterModule.decrement();
console.log("After changes:", counterModule.getCount());
console.log("Direct access to count:", counterModule.count);
count lives only inside the closure — outside code cannot touch it.The Revealing Module Pattern
What this code does: Creates a cartModule using the Revealing Module Pattern. We add three items, print the count and total, then remove the Mouse and print the new total.
const cartModule = (function () {
let items = [];
function addItem(name, price) {
items.push({ name, price });
}
function removeItem(name) {
items = items.filter((item) => item.name !== name);
}
function getTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
function getItemCount() {
return items.length;
}
// Revealed public API
return {
add: addItem,
remove: removeItem,
total: getTotal,
count: getItemCount
};
})();
cartModule.add("Keyboard", 45);
cartModule.add("Mouse", 20);
cartModule.add("Monitor", 150);
console.log("Items in cart:", cartModule.count());
console.log("Total:", cartModule.total());
cartModule.remove("Mouse");
console.log("After removing Mouse — total:", cartModule.total());
The Observer Pattern
What this code does: Builds a small EventEmitter. We subscribe two listeners to the “orderPlaced” event, emit an order, then unsubscribe the email listener and emit again so only the logger still reacts.
class EventEmitter {
#listeners = {};
on(eventName, callback) {
if (!this.#listeners[eventName]) {
this.#listeners[eventName] = [];
}
this.#listeners[eventName].push(callback);
}
off(eventName, callback) {
if (!this.#listeners[eventName]) return;
this.#listeners[eventName] =
this.#listeners[eventName].filter((fn) => fn !== callback);
}
emit(eventName, data) {
if (!this.#listeners[eventName]) return;
this.#listeners[eventName].forEach((callback) => callback(data));
}
}
const emitter = new EventEmitter();
function logOrder(order) {
console.log("Logger heard order:", order.id);
}
function sendConfirmationEmail(order) {
console.log("Emailing confirmation for order:", order.id);
}
emitter.on("orderPlaced", logOrder);
emitter.on("orderPlaced", sendConfirmationEmail);
emitter.emit("orderPlaced", { id: 1001 });
emitter.off("orderPlaced", sendConfirmationEmail);
console.log("--- after unsubscribing the email listener ---");
emitter.emit("orderPlaced", { id: 1002 });
Combining the Module and Observer Patterns
What this code does: Creates a themeStore module that keeps its state private. Two independent subscribers react when we change the theme from “light” to “dark”.
const themeStore = (function () {
let currentTheme = "light";
const listeners = [];
function subscribe(callback) {
listeners.push(callback);
}
function setTheme(newTheme) {
if (newTheme === currentTheme) return;
currentTheme = newTheme;
listeners.forEach((callback) => callback(currentTheme));
}
function getTheme() {
return currentTheme;
}
return {
subscribe,
setTheme,
getTheme
};
})();
// Different parts of an app subscribing independently
themeStore.subscribe((theme) => {
console.log("Header component: switching to", theme, "theme");
});
themeStore.subscribe((theme) => {
console.log("Sidebar component: switching to", theme, "theme");
});
console.log("Initial theme:", themeStore.getTheme());
themeStore.setTheme("dark");
Module vs Observer at a Glance
| Aspect | Module Pattern | Observer Pattern |
|---|---|---|
| Main purpose | Encapsulate state, hide internals | Let parts of the app react to events |
| Built with | Closures (IIFE) or class private fields | A list of callbacks + emit/notify logic |
| Coupling | Consumers depend on a public API | Subject and observers stay decoupled |
| Typical use | Utilities, stores, services | Events, pub/sub, state changes |
Common Mistakes
- Exposing internal state directly from a module
- Forgetting to provide an unsubscribe / off method
- Letting one failing observer callback stop the rest
- Losing
thisinside observer callbacks - Overusing IIFEs when modern classes or ES modules are clearer
Best Practices
- Keep a module’s public API small and intentional
- Prefer class private fields or ES modules in new code
- Always provide a way to unsubscribe from observers
- Wrap observer callbacks in try/catch so one failure doesn’t break others
- Use the Observer Pattern only when parts of the app truly need to stay decoupled
Quick Summary
- The Module Pattern hides internal state and exposes only a controlled public interface
- The Revealing Module Pattern defines everything privately first, then reveals the public API clearly
- The Observer Pattern lets a subject notify many listeners without knowing who they are
- Combining both lets a module keep state private while still notifying the outside world of changes
Try It Yourself
What this code does: Creates a notificationCenter module. Two subscribers react when we send two notifications. At the end we print the full list of notifications that were stored privately.
const notificationCenter = (function () {
let notifications = [];
const listeners = [];
function subscribe(callback) {
listeners.push(callback);
}
function notify(message) {
const entry = { id: notifications.length + 1, message };
notifications.push(entry);
listeners.forEach((callback) => callback(entry));
}
function getAll() {
return [...notifications];
}
return {
subscribe,
notify,
getAll
};
})();
notificationCenter.subscribe((entry) => {
console.log("UI badge: new notification —", entry.message);
});
notificationCenter.subscribe((entry) => {
console.log("Analytics: logging notification #" + entry.id);
});
notificationCenter.notify("Your order has shipped");
notificationCenter.notify("Payment received");
console.log("All notifications:", notificationCenter.getAll());
Tip: Reach for the Module Pattern when you ask “what should stay hidden?” and the Observer Pattern when you ask “who else needs to know when this changes?”
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.