Explain the module pattern
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.
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.
// 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).
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 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
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.
// 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);
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.
| Feature | Module Pattern (IIFE) | ES Modules |
|---|---|---|
| Privacy mechanism | Closure (function scope) | File scope (unexported = private) |
| Multiple instances | Yes (factory function variant) | No (singleton per file) |
| Static analysis | No (runtime pattern) | Yes (tree shaking, IDE support) |
| Dependency declaration | IIFE arguments | import statements |
| Browser support | All browsers (just functions) | Modern browsers only |
| Use case today | Encapsulated state within a module | File-level code organization |
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.
// 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),
};
}
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