Design Patterns

Course 3 · Ch 4
Design Patterns: Module Pattern, Observer, Debounce/Throttle
Three named, reusable solutions to problems that have already shown up, unnamed, in earlier chapters

A design pattern is just a recognised, named solution to a recurring problem — none of the three covered here are new syntax; they're specific ways of combining things already learned (closures, callbacks, timers) to solve specific, common problems cleanly.

The Module Pattern — Closures as Encapsulation

const CounterModule = (function() { let count = 0; // private — never exposed directly return { increment() { count++; return count; }, reset() { count = 0; } }; })(); // IIFE — called immediately, only the returned object survives console.log(CounterModule.increment()); // 1 console.log(CounterModule.count); // undefined — no direct access

This is exactly Intermediate Chapter 2's createBankAccount private-state pattern, with one addition: an IIFE (Immediately Invoked Function Expression) — the surrounding (function() { ... })() — runs the function the instant it's defined, so CounterModule ends up holding the returned object directly, not the function itself. Before native ES modules (Chapter 1) existed, this was the standard way to create a private, self-contained unit of code.

Why "module pattern" if real modules now exist?
Native import/export (Chapter 1) has mostly replaced this pattern's original purpose — but the underlying technique (closures hiding private state, exposing only a deliberate public interface) still shows up constantly inside individual files, classes, and libraries, regardless of module system.

The Observer Pattern — One-to-Many Notifications

class EventEmitter { constructor() { this.listeners = {}; } on(event, callback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); } emit(event, data) { (this.listeners[event] || []).forEach(callback => callback(data)); } } const emitter = new EventEmitter(); emitter.on("login", user => console.log(`Welcome, ${user}`)); emitter.on("login", user => console.log(`Logging access for ${user}`)); emitter.emit("login", "Philip"); // Welcome, Philip // Logging access for Philip

The observer pattern lets multiple unrelated pieces of code (observers) react to something happening, without the thing that happened needing to know who's listening or how many there are. addEventListener (Fundamentals Chapter 8) is observer pattern, built into the browser — EventEmitter here is the same idea, generalized beyond DOM events to any custom event name.

Debounce — Wait Until Activity Stops

function debounce(fn, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } const search = debounce(query => console.log("Searching for:", query), 300); input.addEventListener("input", () => search(input.value)); // Typing "hello" fires the "input" event 5 times, but search() only RUNS once — 300ms after the last keystroke

debounce wraps a function so it only actually runs once activity has genuinely stopped for delay milliseconds — each new call cancels the previous pending timer and starts a fresh one. This is the standard fix for Fundamentals Chapter 9's "input" event firing on every keystroke when a search box should really only query once typing pauses.

Throttle — Allow at Most Once Every X Milliseconds

function throttle(fn, limit) { let inCooldown = false; return function(...args) { if (inCooldown) return; fn(...args); inCooldown = true; setTimeout(() => inCooldown = false, limit); }; } const logScroll = throttle(() => console.log("Scroll position:", window.scrollY), 200); window.addEventListener("scroll", logScroll);

throttle guarantees the wrapped function runs AT MOST once per limit milliseconds, no matter how many times it's called in that window — unlike debounce, the first call goes through immediately, and the function fires regularly during sustained activity rather than waiting for it to stop entirely. scroll events fire dozens of times per second; throttling keeps expensive work (logging, layout calculations) from running that often.

Debounce and throttle solve different problems — don't mix them up
Debounce: "wait for quiet, then run once" — right for search-as-you-type. Throttle: "run regularly, but cap the rate" — right for scroll/resize handlers that need periodic updates throughout continuous activity, not just at the end.
PatternProblem it solvesBuilt from
Module patternPrivate state, public interfaceClosures + IIFE
ObserverMany listeners reacting to one event, decoupledArrays of callbacks + forEach
Debounce"Run once activity stops" (search-as-you-type)setTimeout + clearTimeout
Throttle"Run at most once per interval" (scroll/resize)setTimeout + a boolean flag

Coding Challenges

Challenge 1

Using the module pattern (IIFE returning an object), build a TodoModule with private state (an array) and a public interface of addTodo(text) and getAll(). Add 3 todos and log the result of getAll(), then confirm TodoModule.todos is undefined.

📄 View solution
Challenge 2

Using the EventEmitter class from this chapter, create an instance and register two separate listeners for an "orderPlaced" event — one logging a confirmation message, one logging that an email should be sent. Emit the event once with an order ID and confirm both listeners run.

📄 View solution
Challenge 3

Using the debounce function from this chapter, wrap a function that logs "Saving..." with a 500ms delay. Call the debounced version 5 times in quick succession (e.g. in a loop with no delay between calls) and explain in a comment why "Saving..." only logs once.

📄 View solution

Chapter 4 Quick Reference

  • Module pattern — IIFE + closure, returning a deliberate public interface, hiding private state
  • IIFE: (function() { ... })() — runs immediately, only what it returns survives
  • Observer pattern — register listeners, emit events; addEventListener is this pattern, built-in
  • Debounce — runs once, after activity stops for a set delay (search-as-you-type)
  • Throttle — runs at most once per interval, during continuous activity (scroll/resize)
  • None of these are new syntax — all three combine closures, timers, and callbacks already covered
  • Next chapter: working with APIs — pagination, rate limits, caching strategies