Design PatternsJavaScriptMedium

Explain the module pattern

01

The Short Answer

The module pattern is a design pattern that uses closures to create private scope — encapsulating variables and functions so they can't be accessed or modified from outside. Before ES modules existed, it was the primary way to avoid polluting the global namespace and to create truly private state in JavaScript. The pattern works by wrapping code in an IIFE (Immediately Invoked Function Expression) and returning only the public API. While ES modules have largely replaced it for file-level encapsulation, the underlying concept of closure-based privacy is still fundamental to JavaScript.

02

The Classic Module Pattern

The pattern uses an IIFE to create a private scope. Variables declared inside the IIFE are invisible to the outside world — they only exist within that function's closure. The IIFE returns an object containing the public methods, which have access to the private variables through closure. This gives you true encapsulation: private state that can only be read or modified through the methods you explicitly expose.

module-pattern.tstypescript
// The Module Pattern — IIFE + closure = private scope
const Counter = (() => {
  // Private — not accessible from outside
  let count = 0;
  const MAX_COUNT = 100;

  function validate(value: number): boolean {
    return value >= 0 && value <= MAX_COUNT;
  }

  // Public API — returned object
  return {
    increment() {
      if (validate(count + 1)) {
        count++;
      }
      return count;
    },
    decrement() {
      if (validate(count - 1)) {
        count--;
      }
      return count;
    },
    getCount() {
      return count;
    },
    reset() {
      count = 0;
      return count;
    },
  };
})();

// Usage
Counter.increment(); // 1
Counter.increment(); // 2
Counter.getCount();  // 2
Counter.reset();     // 0

// Private members are truly inaccessible
// Counter.count       → undefined (not a property of the returned object)
// Counter.validate    → undefined
// Counter.MAX_COUNT   → undefined

The key insight: count, MAX_COUNT, and validate exist only inside the IIFE's closure. The returned methods (increment, decrement, etc.) can access them because they were defined in the same scope — but nothing outside can. This is genuine privacy, not convention-based (like _privateVar naming).

03

Revealing Module Pattern

A popular variation is the Revealing Module Pattern, where you define all functions as private and then explicitly reveal (return) the ones you want to be public. This makes it clearer which functions are part of the public API and keeps the return statement clean — it's just a mapping of public names to private implementations.

revealing-module.tstypescript
// Revealing Module Pattern — define private, reveal public
const UserService = (() => {
  // All implementation is private
  const users: Map<string, { name: string; email: string }> = new Map();

  function generateId(): string {
    return Math.random().toString(36).substring(2, 9);
  }

  function validateEmail(email: string): boolean {
    return email.includes('@') && email.includes('.');
  }

  function addUser(name: string, email: string): string | null {
    if (!validateEmail(email)) return null;
    const id = generateId();
    users.set(id, { name, email });
    return id;
  }

  function getUser(id: string) {
    return users.get(id) ?? null;
  }

  function removeUser(id: string): boolean {
    return users.delete(id);
  }

  function getUserCount(): number {
    return users.size;
  }

  // Reveal only the public API
  return {
    add: addUser,
    get: getUser,
    remove: removeUser,
    count: getUserCount,
    // generateId, validateEmail, users — all stay private
  };
})();

UserService.add('Alice', 'alice@example.com'); // returns id
UserService.count(); // 1
// UserService.users → undefined
// UserService.validateEmail → undefined
04

Module Pattern with Dependencies

Modules often depend on other modules or global libraries. You can pass dependencies as arguments to the IIFE, making them explicit and allowing for easier testing (you can pass mocks). This pattern was especially important before ES modules, when managing dependencies between script files was manual and error-prone.

module-with-deps.tstypescript
// Pass dependencies as IIFE arguments
const Logger = ((console, storage) => {
  const LOG_KEY = 'app_logs';
  let logs: string[] = [];

  function formatMessage(level: string, message: string): string {
    return `[${new Date().toISOString()}] ${level}: ${message}`;
  }

  function log(message: string) {
    const formatted = formatMessage('INFO', message);
    logs.push(formatted);
    console.log(formatted);
  }

  function error(message: string) {
    const formatted = formatMessage('ERROR', message);
    logs.push(formatted);
    console.error(formatted);
  }

  function persist() {
    storage.setItem(LOG_KEY, JSON.stringify(logs));
  }

  function clear() {
    logs = [];
    storage.removeItem(LOG_KEY);
  }

  return { log, error, persist, clear };
})(window.console, window.localStorage);

// Dependencies are explicit — easy to mock for testing:
// const TestLogger = ((mockConsole, mockStorage) => { ... })(mockConsole, mockStorage);
05

Module Pattern vs ES Modules

ES modules (import/export) have largely replaced the IIFE-based module pattern for file-level encapsulation. However, the patterns serve slightly different purposes. ES modules provide file-level scope (everything not exported is private to the file). The module pattern provides closure-based privacy within a single scope — useful for creating multiple instances or encapsulating state within a larger module.

FeatureModule Pattern (IIFE)ES Modules
Privacy mechanismClosure (function scope)File scope (unexported = private)
Multiple instancesYes (factory function variant)No (singleton per file)
Static analysisNo (runtime pattern)Yes (tree shaking, IDE support)
Dependency declarationIIFE argumentsimport statements
Browser supportAll browsers (just functions)Modern browsers only
Use case todayEncapsulated state within a moduleFile-level code organization
06

Modern Usage — Closure-Based Privacy

Even with ES modules, the closure-based privacy concept from the module pattern lives on. Factory functions that return objects with private state, React hooks that encapsulate state behind a clean API, and singleton services all use the same fundamental idea: a function creates a scope, captures variables in closure, and returns a limited public interface.

modern-usage.tstypescript
// Factory function — module pattern without IIFE
function createStore<T>(initialState: T) {
  // Private state — captured in closure
  let state = initialState;
  const listeners = new Set<(state: T) => void>();

  // Public API
  return {
    getState: () => state,
    setState(newState: Partial<T>) {
      state = { ...state, ...newState };
      listeners.forEach((listener) => listener(state));
    },
    subscribe(listener: (state: T) => void) {
      listeners.add(listener);
      return () => listeners.delete(listener); // unsubscribe
    },
  };
}

// Each call creates independent private state
const userStore = createStore({ name: '', loggedIn: false });
const cartStore = createStore({ items: [], total: 0 });

// React hook — same concept
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial); // private to this hook instance
  return {
    count,
    increment: () => setCount((c) => c + 1),
    decrement: () => setCount((c) => c - 1),
    reset: () => setCount(initial),
  };
}
07

Why Interviewers Ask This

This question tests understanding of closures, encapsulation, and JavaScript's evolution. Interviewers want to see that you understand how closures create private scope (the core mechanism), can explain why the pattern existed (no native modules before ES2015), know the difference between the classic and revealing variants, understand how the concept lives on in modern code (factories, hooks, stores), and can compare it to ES modules and explain when each is appropriate. It's a question that connects language fundamentals (closures) to architectural patterns (encapsulation).

Quick Revision Cheat Sheet

Core mechanism: IIFE creates private scope, returns public API object

Privacy: Variables in closure are truly private — not accessible from outside

Revealing variant: Define all functions private, return a mapping of public names

Dependencies: Pass as IIFE arguments — makes them explicit and mockable

Modern equivalent: ES modules for files, factory functions for instances

Still relevant?: The closure concept is everywhere — hooks, stores, factories