Senior1 questionFull Guide

JavaScript Design Patterns Interview Questions

JavaScript design patterns are crucial in interviews because they demonstrate your ability to write scalable, maintainable, and real-world production-quality code.

The Mental Model

Picture a master chef's recipe book. Not a book of specific dishes, but a book of cooking techniques — "braising", "emulsifying", "reducing". Each technique is a proven solution to a class of cooking problems. When a new dish needs to trap moisture while tenderising tough meat, the chef reaches for braising without inventing a new approach from scratch. The technique has been refined over decades and its tradeoffs are well understood. Design patterns are the software equivalent. They are not code you copy — they are proven solutions to recurring problems in software design. When your code needs one object to notify many others about changes, you reach for the Observer pattern. When you need exactly one instance of something to be shared across an entire application, you reach for Singleton. When you need to create objects without specifying their exact class, you reach for Factory. The key insight: patterns are a shared vocabulary. Saying "use an Observer here" communicates the entire structure — event source, subscribers, notification mechanism — in one word. This shared language is why senior engineers discuss architecture without drawing pictures. Knowing the patterns does not make you follow them blindly — it gives you a toolkit of solutions whose consequences and tradeoffs you understand before you write the first line of code. Interviews use design patterns as a signal that you have solved these classes of problems before.

The Explanation

Why design patterns matter in JavaScript interviews

JavaScript's flexibility means the same structural problem can be solved dozens of ways. Design patterns are not about following academic rules — they are about communicating that your solution is intentional, recognisable, and carries known tradeoffs. A senior engineer who says "I used a Singleton for the database connection pool" is communicating more than just the code — they are communicating awareness that only one instance should exist, that it needs lazy initialisation, and that global state in this form has testability implications.

Five patterns appear in virtually every senior frontend interview: Observer, Singleton, Module, Factory, and Decorator. Each one maps to something you use daily in modern JavaScript.

Observer pattern — the foundation of all event systems

The Observer pattern defines a one-to-many relationship: one subject maintains a list of observers and notifies them all when its state changes. Observers subscribe and unsubscribe independently. The subject never needs to know the implementation details of any observer.

class EventEmitter {
  constructor() {
    this.events = {}   // eventName → [handler, handler, ...]
  }
 
  on(event, handler) {
    if (!this.events[event]) this.events[event] = []
    this.events[event].push(handler)
    return this   // enable method chaining
  }
 
  off(event, handler) {
    if (!this.events[event]) return this
    this.events[event] = this.events[event].filter(h => h !== handler)
    return this
  }
 
  emit(event, ...args) {
    if (!this.events[event]) return this
    this.events[event].forEach(handler => handler(...args))
    return this
  }
 
  once(event, handler) {
    const wrapper = (...args) => {
      handler(...args)
      this.off(event, wrapper)   // auto-remove after first call
    }
    return this.on(event, wrapper)
  }
}
 
// Usage
const emitter = new EventEmitter()
 
function onLogin(user) {
  console.log(`${user.name} logged in`)
}
 
emitter.on('login', onLogin)
emitter.on('login', user => sendWelcomeEmail(user))
 
emitter.emit('login', { name: 'Alice' })
// "Alice logged in"
// sendWelcomeEmail called
 
emitter.off('login', onLogin)   // unsubscribe specific handler
emitter.emit('login', { name: 'Bob' })
// Only sendWelcomeEmail called — onLogin was removed

This is the pattern behind document.addEventListener, Node.js's EventEmitter, Vue's $emit/$on, and Redux's store.subscribe(). Implementing it from scratch is one of the most common senior interview tasks.

Pub/Sub pattern — Observer with a message bus

Pub/Sub extends Observer with a central message broker (the bus) so publishers and subscribers are completely decoupled — they never reference each other directly. This is the distinction interviewers probe: in Observer, the subject knows its observers exist. In Pub/Sub, the publisher and subscriber only know about the bus.

class PubSub {
  constructor() {
    this.subscribers = {}
  }
 
  subscribe(topic, handler) {
    if (!this.subscribers[topic]) this.subscribers[topic] = []
    this.subscribers[topic].push(handler)
 
    // Return unsubscribe function — cleaner API than calling unsubscribe() separately
    return () => {
      this.subscribers[topic] = this.subscribers[topic].filter(h => h !== handler)
    }
  }
 
  publish(topic, data) {
    if (!this.subscribers[topic]) return
    this.subscribers[topic].forEach(handler => handler(data))
  }
}
 
const bus = new PubSub()
 
const unsubscribe = bus.subscribe('cart:updated', (cart) => {
  updateCartUI(cart)
})
 
// Publisher: knows nothing about who is listening
bus.publish('cart:updated', { items: 3, total: 299 })
 
// Clean up when component unmounts
unsubscribe()

Singleton pattern — exactly one instance

The Singleton ensures a class has only one instance and provides a global access point to it. Useful for shared resources like database connections, configuration stores, logging services, and caches where creating multiple instances would cause inconsistency or resource waste.

class DatabaseConnection {
  constructor(config) {
    if (DatabaseConnection._instance) {
      return DatabaseConnection._instance   // return existing instance
    }
    this.connection = createConnection(config)   // expensive setup — only once
    DatabaseConnection._instance = this
  }
 
  static getInstance(config) {
    if (!DatabaseConnection._instance) {
      new DatabaseConnection(config)
    }
    return DatabaseConnection._instance
  }
 
  query(sql) {
    return this.connection.execute(sql)
  }
}
 
const db1 = DatabaseConnection.getInstance({ host: 'localhost' })
const db2 = DatabaseConnection.getInstance()
 
console.log(db1 === db2)   // true — same instance

Modern JavaScript Singleton using ES modules (the idiomatic approach):

// config.js — module scope IS the Singleton pattern
// ES modules are cached — this object is created once and shared everywhere
const config = {
  apiUrl: process.env.API_URL,
  timeout: 5000,
  retries: 3,
}
 
export default config
 
// In every file that imports config:
import config from './config.js'
// The same object — module system provides Singleton behaviour for free

When Singleton is dangerous:

// Singletons make testing difficult — shared state leaks between tests
// Test 1 modifies the Singleton state
// Test 2 receives contaminated state — tests fail intermittently
 
// The fix for testing: dependency injection instead of direct Singleton access
// Pass the instance as a parameter rather than accessing it globally
function processOrder(order, db = DatabaseConnection.getInstance()) {
  return db.query(`INSERT INTO orders...`)
}
 
// In tests: pass a mock instead of the real Singleton
processOrder(testOrder, mockDb)

Module pattern — private state via closures

The Module pattern uses an IIFE and closure to create private state that is accessible only through the explicitly returned public API. It was the primary way to encapsulate code before ES6 modules.

// Classic Module pattern — IIFE creates private scope
const CartModule = (function() {
  // Private — not accessible outside
  let items    = []
  let discount = 0
 
  function calculateTotal() {
    const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
    return subtotal * (1 - discount)
  }
 
  // Public API — only what is returned is accessible
  return {
    addItem(item)    { items.push(item) },
    removeItem(id)   { items = items.filter(i => i.id !== id) },
    setDiscount(pct) { discount = pct / 100 },
    getTotal()       { return calculateTotal() },
    getCount()       { return items.length },
  }
})()
 
CartModule.addItem({ id: 1, name: 'Book', price: 299, qty: 2 })
CartModule.setDiscount(10)
console.log(CartModule.getTotal())   // 538.2
console.log(CartModule.items)        // undefined — private, not accessible

Modern ES6 modules provide this pattern natively — variables not exported are private to the module. The closure-based IIFE approach is now mainly seen in legacy codebases and in interview questions testing closure understanding.

Factory pattern — create objects without specifying the class

The Factory pattern provides an interface for creating objects where the exact type is determined by input parameters or configuration, rather than by the caller invoking specific constructors directly. It centralises object creation logic and decouples the caller from implementation details.

// Without Factory — caller must know all classes
const card   = new CreditCardPayment(details)
const paypal = new PaypalPayment(details)
const upi    = new UPIPayment(details)
 
// With Factory — caller just describes what it needs
function createPaymentMethod(type, details) {
  const methods = {
    credit:  () => new CreditCardPayment(details),
    paypal:  () => new PaypalPayment(details),
    upi:     () => new UPIPayment(details),
    crypto:  () => new CryptoPayment(details),
  }
 
  const factory = methods[type]
  if (!factory) throw new Error(`Unknown payment type: ${type}`)
  return factory()
}
 
// Caller only knows the type string — not the class
const payment = createPaymentMethod('upi', userDetails)
payment.process(amount)   // works regardless of which class was created

React's createElement is a factory — you pass a type string or component function and React creates the appropriate element. Hooks like useReducer use the Factory concept — the action type determines which reducer branch handles state creation.

Decorator pattern — add behaviour without modifying the original

The Decorator pattern wraps an object or function and adds new behaviour without changing the original's source code. The wrapped entity maintains its interface, and the decorator adds to it. Decorators can be stacked — multiple decorators wrap the same core.

// Function decorators — wrap a function to add behaviour
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with`, args)
    const result = fn(...args)
    console.log(`${fn.name} returned`, result)
    return result
  }
}
 
function withErrorHandling(fn) {
  return function(...args) {
    try {
      return fn(...args)
    } catch (err) {
      console.error(`${fn.name} failed:`, err.message)
      return null
    }
  }
}
 
function withCache(fn) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const result = fn(...args)
    cache.set(key, result)
    return result
  }
}
 
// Stack decorators — each wraps the previous
function fetchUser(id) {
  // database call
}
 
const safeLoggedCachedFetch = withLogging(withErrorHandling(withCache(fetchUser)))
safeLoggedCachedFetch(42)
// Logged, error-safe, and cached — original function unchanged

TypeScript and the upcoming JavaScript decorators proposal bring this pattern to class syntax. React Higher-Order Components (HOC) are the Decorator pattern applied to components — withAuth(Component), withLoading(Component).

Strategy pattern — swap algorithms at runtime

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The client selects which algorithm to use at runtime without knowing the implementation details of any of them.

// Sorting strategies
const strategies = {
  bubble:    (arr) => bubbleSort([...arr]),
  quick:     (arr) => quickSort([...arr]),
  merge:     (arr) => mergeSort([...arr]),
  builtin:   (arr) => [...arr].sort((a, b) => a - b),
}
 
class Sorter {
  constructor(strategy = 'builtin') {
    this.strategy = strategies[strategy]
  }
 
  setStrategy(name) {
    this.strategy = strategies[name]
  }
 
  sort(data) {
    return this.strategy(data)
  }
}
 
const sorter = new Sorter('quick')
sorter.sort([3, 1, 4, 1, 5, 9])
 
// Switch strategy without changing the client code
sorter.setStrategy('merge')
sorter.sort([3, 1, 4, 1, 5, 9])

React's context and dependency injection both implement Strategy. Providing different implementations of a service through props or context rather than hardcoding it is the Strategy pattern in a component architecture.

Common Misconceptions

⚠️

Many devs think Singleton is always a good pattern for shared resources — but actually Singleton is one of the most frequently misused patterns in JavaScript. It introduces hidden global state that makes functions hard to test, creates tight coupling between modules, and causes subtle bugs when state from one test leaks into another. ES modules provide Singleton behaviour naturally for configuration, and dependency injection is generally preferable to direct Singleton access for services.

⚠️

Many devs think Observer and Pub/Sub are the same pattern — but actually the key difference is coupling. In Observer, the subject holds references to its observers directly — both sides know each other exists. In Pub/Sub, publishers and subscribers both interact only with a central broker and never reference each other. Observer is tighter coupling; Pub/Sub enables complete decoupling between event source and handler.

⚠️

Many devs think the Module pattern is obsolete with ES6 — but actually understanding the closure-based Module pattern is still tested because it demonstrates mastery of closures and scope. ES6 modules provide the same encapsulation benefit at the language level, but the IIFE-based Module pattern appears in legacy codebases, bundled output, browser scripts that cannot use import/export, and interview questions specifically designed to test closure understanding.

⚠️

Many devs think design patterns are templates to copy and paste — but actually patterns are solutions to specific classes of problems with specific tradeoffs. Applying Observer when you need simple function composition adds unnecessary complexity. Applying Singleton to a stateless utility function is pointless. The skill is recognising which problem class you have and whether a pattern's tradeoffs are acceptable for your context.

⚠️

Many devs think decorators in JavaScript are the same as TypeScript decorators — but actually TypeScript decorators are an experimental feature using a specific syntax. JavaScript function decorators (higher-order functions that wrap other functions) are plain JavaScript available in any environment. The pattern is the same; the syntax and lifecycle hooks differ significantly.

⚠️

Many devs think Factory is just a function that creates objects — but actually the defining characteristic of the Factory pattern is that it decouples the caller from the specific class being instantiated. A constructor called directly is not a factory. A factory determines which class to instantiate based on parameters, hiding that decision from the caller and allowing it to change without affecting calling code.

Where You'll See This in Real Code

Node.js's EventEmitter class is the Observer pattern shipped as a core language primitive — every major Node.js API (HTTP servers, file streams, database drivers, child processes) extends EventEmitter, which means understanding the Observer pattern is required to understand Node.js's fundamental programming model.

Redux's store is a Singleton by design — a single store holds the entire application state, and every component that connects to it receives the same instance. Redux Toolkit's configureStore creates this Singleton and its middleware system is a pipeline of decorators, each wrapping the dispatch function to add logging, async handling, or devtools integration.

React's Higher-Order Component pattern (withAuth, withRouter, connect) is the Decorator pattern applied to components — a function receives a component and returns a new component with additional behaviour, leaving the original component's code completely unchanged and allowing decorators to be stacked and composed.

Webpack and Vite both use the Factory pattern for loader and plugin resolution — given a file extension or plugin name, they instantiate the correct loader class from a registry without the core bundling code needing to import any specific loader directly. Adding a new loader type does not require changing the bundling core.

JavaScript interview platforms including LeetCode, HackerRank, and JSPrep use the Strategy pattern in their test runner architecture — different languages and runtime environments are plugged in as strategies, so the same test orchestration code runs JavaScript, Python, or Java tests by swapping the execution strategy without changing the evaluation logic.

The Pub/Sub pattern is the architectural foundation of micro-frontend communication — when multiple independent frontend applications need to coordinate (a React application, an Angular application, and a vanilla JavaScript widget all running on the same page), they communicate through a shared event bus rather than importing from each other, maintaining complete independence while enabling coordination.

Interview Cheat Sheet

  • Observer: subject notifies its own list of observers — both sides know each other exist
  • Pub/Sub: publisher and subscriber communicate through a broker — completely decoupled
  • Singleton: one instance, global access point — ES modules provide this natively for configuration
  • Singleton danger: hidden global state, hard to mock in tests — prefer dependency injection
  • Module pattern: IIFE + closure = private state, public API — ES6 modules replace this at language level
  • Factory: centralises object creation, decouples caller from specific class — returns different classes based on input
  • Decorator: wraps function or object to add behaviour without modifying original — HOCs, middleware, withLogging
  • Strategy: swap algorithms at runtime without changing the caller — dependency injection is Strategy applied to services
  • EventEmitter implements Observer — on/off/emit/once are the four core methods
  • Decorators can be stacked — each wraps the previous, building a pipeline of behaviours
💡

How to Answer in an Interview

  • 1.For EventEmitter implementation, always include once() in addition to on/off/emit — it shows you know the full API and have used it in production, not just read about it
  • 2.When asked about Singleton, proactively mention the testability problem — it signals senior thinking and opens a conversation about dependency injection as the alternative
  • 3.Connect every pattern to a framework you know: Observer is EventEmitter and Redux subscribe, Factory is React.createElement, Decorator is HOCs and middleware, Strategy is context providers
  • 4.The Observer vs Pub/Sub distinction separates candidates who have read about patterns from those who have implemented them — lead with the coupling distinction clearly
  • 5.When asked to implement a pattern from scratch, spend 30 seconds describing the problem it solves before writing code — interviewers are testing whether you understand the pattern, not just your typing speed
  • 6.Module pattern questions are really closure questions in disguise — frame your explanation around what the IIFE creates (a private scope) and what the closure maintains (access to that scope after the IIFE executes

Practice Questions

1 question
#01

Implementing the Factory Design Pattern in JavaScript Functions

HardFunctions PRO💡 Think about how you can use a single function to create objects of different classes, and how this can help in managing complexity and improving code reusability.

Related Topics

JavaScript Memoization Interview Questions
Intermediate·4–8 Qs
JavaScript Higher-Order Functions Interview Questions
Intermediate·5–8 Qs
JavaScript Prototype & Prototypal Inheritance Interview Questions
Advanced·6–10 Qs
JavaScript Event Propagation Interview Questions
Intermediate·4–8 Qs
JavaScript ES Modules Interview Questions
Intermediate·4–6 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz