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

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.
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
// 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
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.
const counter = (() => { let count = 0; // private return { increment: () => ++count, value: () => count, };})();
counter.increment(); // 1counter.count; // undefinedIntermediate · 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.
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)); }, };}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.
- Confirm the singleton's internal state is genuinely shared across tests.
- Decide whether the application actually needs exactly one logger instance, or just convenient shared access.
- Replace the singleton with a factory that creates an isolated instance per test, injected where the app needs it.
- 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.