Skip to content
Intermediate15 min study

JavaScript design patterns

Recognize module, singleton, observer, and factory patterns as solutions to specific structural problems, not decoration.

Question progress0 / 10 completed
Start the lesson
JavaScript design patterns visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain javascript design patterns in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

A design pattern is a named, reusable solution to a recurring structural problem. The module pattern hides implementation behind a public API, singleton ensures exactly one shared instance, observer lets multiple listeners react to an event without tight coupling, and factory centralizes how related objects get created.

Think of it like this: A design pattern is like a cooking technique, not a specific dish. 'Braising' isn't a meal by itself, but knowing when braising is the right technique — tough cuts of meat, long cook times — is what separates someone who followed one recipe once from someone who actually understands cooking well enough to solve a new problem.

One-line definition: Recognize module, singleton, observer, and factory patterns as solutions to specific structural problems, not decoration.

02Mental model

Each pattern trades something for something: module trades global convenience for encapsulation; singleton trades testability and flexibility for guaranteed shared state; observer trades some indirection for decoupling publishers from subscribers; factory trades a direct constructor call for the flexibility to change what gets created later. Naming the trade-off is what shows real understanding, not just naming the pattern.

03Step by step

  • Identify the actual structural problem before reaching for a named pattern.
  • Use the module pattern, or ES modules, to hide implementation details behind a small public surface.
  • Use singleton only when there's a genuine reason exactly one instance must exist app-wide.
  • Use observer or pub-sub to decouple something that changes from things that react to the change.
  • Use factory when object creation logic needs to be centralized or vary by runtime condition.

04Working example

JavaScript
// Observer / pub-subfunction createEmitter() {  const listeners = new Map();  return {    on(event, fn) {      if (!listeners.has(event)) listeners.set(event, new Set());      listeners.get(event).add(fn);      return () => listeners.get(event).delete(fn);    },    emit(event, payload) {      listeners.get(event)?.forEach((fn) => fn(payload));    },  };}
const bus = createEmitter();const unsubscribe = bus.on('cart:updated', (cart) => render(cart));bus.emit('cart:updated', { items: 3 });

The emitter decouples whatever changes the cart from whatever needs to react to it — neither side needs to know about the other, only about the shared event name. on returns an unsubscribe function so listeners can clean themselves up, which is the detail most hand-rolled event systems forget.

05Where it is used

  • Module pattern for encapsulating a cache or configuration behind a small API
  • Singleton for a single shared connection pool, logger, or app-wide config object
  • Observer for event buses, DOM events themselves, and React's own subscription-based hooks
  • Factory for creating different notification types such as email, SMS, or push behind one createNotifier(type) call

06Common mistakes

  • Reaching for singleton by default, making code hard to test because state persists across tests
  • Building an observer system with no way to unsubscribe, causing the same memory-leak pattern as forgotten event listeners
  • Using a factory when a plain constructor or object literal would be clearer and there's no real variation to centralize
  • Treating 'design pattern' as a badge of sophistication rather than a fit-for-problem decision

07Interview answer

How to say it out loud: "A design pattern is a named, reusable solution to a structural problem that keeps showing up, and every one of them has a real cost, not just a benefit. The module pattern hides implementation details behind a small public API — ES modules do that natively now. Singleton guarantees exactly one shared instance exists, but that shared state is exactly what makes testing harder, since one test's mutation can leak into the next unless it's explicitly reset. Observer, or pub-sub, decouples something that changes from the things that react to it — which is basically how DOM events work — but only pays off if you remember to give it an unsubscribe mechanism, or you get the same kind of memory leak as a forgotten event listener. And factory centralizes the decision of what to construct, which earns its keep when that decision genuinely needs to vary, but is unnecessary ceremony when it doesn't."

Name the specific problem the pattern solves and its cost — a singleton's cost is testability, an observer's cost is indirection — instead of reciting pattern definitions from a book.

Why is a singleton often harder to unit test than a factory-created instance?

A singleton holds shared state across the entire process lifetime, so one test's mutations can leak into the next test unless it's explicitly reset; a factory creates a fresh, isolated instance per call, so each test can start from a clean, independent state without extra teardown logic.

DDConcept deep dives

Deep dive 1

Every pattern is a named trade-off, not a free improvement

A pattern earns its name by solving a specific recurring structural problem, and it always costs something to adopt: module encapsulation costs direct access convenience, singleton costs testability and flexibility, observer costs a layer of indirection between cause and effect, factory costs a level of abstraction over direct construction. Reciting a pattern's definition is weak; naming its specific cost for the situation at hand is what shows real understanding.

  • Ask 'what structural problem does this actually solve here' before applying a pattern.
  • A pattern applied to a problem that doesn't have the relevant variability is pure overhead.
  • The cost of a pattern is often the more interesting interview answer than its benefit.

Deep dive 2

Encapsulation and shared state are separate design axes

The module pattern and the singleton pattern are often taught together but solve different problems: module pattern is about hiding implementation details behind a public API, regardless of how many instances exist, while singleton is specifically about guaranteeing exactly one shared instance exists at all. A module-pattern factory that creates many independent, encapsulated instances is common and often preferable — conflating 'encapsulated' with 'must be singular' leads to unnecessary shared global state and the testing problems that come with it.

  • ES modules provide file-level encapsulation without forcing a singleton.
  • A factory can produce encapsulated instances that are each independent of one another.
  • Singleton should be a deliberate choice justified by a genuine need for exactly one instance, not a default.

Deep dive 3

Observer patterns need explicit lifecycle management

An event emitter, a pub-sub bus, or any subscription-based system creates a live reference from the publisher to each subscriber's callback — exactly the same reachability relationship that makes forgotten event listeners and timers leak memory. A complete observer implementation needs an explicit unsubscribe mechanism, and calling code needs to actually call it during its own teardown, or the subscription — and everything it closes over — outlives its intended owner indefinitely.

  • An unsubscribe function returned from a subscribe call is a common, clean API shape.
  • This is the same underlying leak pattern as DOM event listeners, just without addEventListener/removeEventListener's built-in symmetry.
  • React's own built-in subscription-based hooks (like useSyncExternalStore) handle this cleanup automatically as part of their contract.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Coding · 1 min · Question 1What problem does the module pattern solve?Open model answer

Model answer

It creates a private, encapsulated scope for implementation details, exposing only a deliberately chosen public API. Consumers can't reach into or accidentally mutate internal state directly, which ES modules now provide natively at the file level.

JavaScript
const counter = (() => {  let count = 0;                 // private  return {    increment: () => ++count,    value: () => count,  };})();
counter.increment(); // 1counter.count;       // undefined
Open question page →
Intermediate · Conceptual · 1 min · Question 2What's the main cost of using a singleton?Open model answer

Model answer

It introduces shared, persistent state across the entire application lifetime, which makes unit testing harder — one test's mutation of the singleton can leak into the next test unless the singleton is explicitly reset between tests.

Open question page →
Intermediate · Conceptual · 1 min · Question 3What is the observer pattern actually decoupling?Open model answer

Model answer

It decouples the code that produces an event or change from the code that reacts to it — publishers don't need to know who's listening, and subscribers don't need to know who's publishing, only the shared event contract between them.

Open question page →
Intermediate · Coding · 1 min · Question 4Why is an unsubscribe mechanism critical to an observer implementation?Open model answer

Model answer

Without a way to remove a listener, every subscription lives for the lifetime of the emitter itself, which is exactly the retained-closure memory-leak pattern that forgotten event listeners and timers also create.

JavaScript
function createEmitter() {  const listeners = new Set();  return {    on(fn) {      listeners.add(fn);      return () => listeners.delete(fn); // return an unsubscribe    },    emit(data) { listeners.forEach((fn) => fn(data)); },  };}
Open question page →
Intermediate · Conceptual · 1 min · Question 5What does the factory pattern centralize that a set of scattered constructor calls doesn't?Open model answer

Model answer

The decision logic for exactly what gets created and how, in one place — so that logic can vary based on configuration or runtime conditions without every call site needing to know or duplicate that decision.

Open question page →
Intermediate · Conceptual · 1 min · Question 6When is reaching for a named design pattern actually a bad sign in a code review?Open model answer

Model answer

When it adds indirection or abstraction for a problem that doesn't actually have the variability or coupling the pattern is meant to solve — a factory with only one possible product, or a singleton with no real reason to forbid multiple instances, are both overhead without benefit.

Open question page →
Advanced · Conceptual · 1 min · Question 7How does the revealing module pattern differ from a basic module pattern?Open model answer

Model answer

It defines all functions and variables privately first, then returns an object literal that simply maps public names to the already-defined private references, making it clearer at a glance which internal pieces are exposed publicly.

Open question page →
Advanced · Conceptual · 1 min · Question 8Is React's Context API an implementation of the observer pattern?Open model answer

Model answer

It shares similarities — providers publish a value and consumers subscribe to changes — but Context updates propagate through React's render cycle rather than a manually managed listener list, so it's more accurate to call it dependency injection with observer-like update propagation.

Open question page →
Advanced · Conceptual · 1 min · Question 9What's a lazy singleton, and why might it be preferred over an eagerly created one?Open model answer

Model answer

It defers creating the singleton instance until the first time it's actually requested, rather than at module load — useful when the instance is expensive to construct and might never be needed in a given execution path.

Open question page →
Advanced · Conceptual · 1 min · Question 10Can the factory pattern be combined with the strategy pattern?Open model answer

Model answer

Yes — a factory can select and instantiate a specific strategy implementation based on runtime configuration, centralizing both which algorithm gets used and how its object is constructed in one place.

Open question page →

SCScenario questions

Scenario 1

A codebase has a Logger singleton with a getInstance() method, and the test suite has become slow and flaky because log output and internal counters leak between unrelated test files.

  1. Confirm the singleton's internal state is genuinely shared across tests.
  2. Decide whether the application actually needs exactly one logger instance, or just convenient shared access.
  3. Replace the singleton with a factory that creates an isolated instance per test, injected where the app needs it.
  4. Keep a single shared instance only at the application's actual composition root, not baked into the class itself.
Reveal worked answer

I would separate 'the app happens to use one logger' from 'a logger must enforce single-instance-ness.' I'd change Logger to a plain factory-created class, construct one instance at the application's real entry point for production use, and let each test create its own isolated instance instead of sharing the singleton's global state. This removes the cross-test leakage without losing the single-instance behavior production actually wants.

Verify and go deeper