The Module Pattern in JavaScript
The module pattern is one of the most useful design patterns in JavaScript. It provides a way to encapsulate code, create private state, and expose a clean public API. While ES6 modules have largely replaced the traditional module pattern, understanding it helps you appreciate modern JavaScript and gives you tools for scenarios where you need more control over encapsulation.
Encapsulation through closures
The module pattern uses closures to create private state that cannot be accessed from outside the module. It combines IIFEs (Immediately Invoked Function Expressions) with closures to achieve encapsulation. The key idea is simple: wrap your code in a function, create variables and functions inside that function, and return only what you want to be public.
const MyModule = (function() {
// Private variables and functions
let privateCounter = 0;
function privateFunction() {
return "This is private";
}
// Public API
return {
publicMethod: function() {
privateCounter++;
return privateFunction();
},
getCounter: function() {
return privateCounter;
}
};
})();
console.log(MyModule.publicMethod()); // This is private
console.log(MyModule.getCounter()); // 1
// These would be undefined or throw errors:
// console.log(MyModule.privateCounter);
// console.log(MyModule.privateFunction());
IIFE: The Foundation
An IIFE (Immediately Invoked Function Expression) is a function that runs immediately after being defined. It’s the foundation of the module pattern because it creates a new scope: a place where your variables and functions can’t be accessed from outside.
// Basic IIFE
(function() {
const secret = "I'm hidden from the outside";
console.log(secret); // Works inside
})();
// console.log(secret); // ReferenceError: secret is not defined
The key insight is that the IIFE creates a closure. The function inside “remembers” the environment where it was created, even after it finishes executing. This is what allows the returned object to access private variables.
The revealing module pattern
A popular variation is the revealing module pattern, where you define all functions and variables privately, then explicitly expose references to the ones you want to be public. This makes it clear exactly what is part of the public API.
const UserManager = (function() {
// Private state
const users = [];
const maxUsers = 100;
// Private functions
function validateUser(user) {
return user && typeof user.name === "string" && user.name.length > 0;
}
function generateId() {
return Math.random().toString(36).substring(2, 9);
}
// Public API - explicitly revealing what we want to expose
return {
addUser: function(user) {
if (!validateUser(user)) {
throw new Error("Invalid user object");
}
if (users.length >= maxUsers) {
throw new Error("Maximum users reached");
}
const newUser = { ...user, id: generateId() };
users.push(newUser);
return newUser;
},
removeUser: function(id) {
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return false;
}
users.splice(index, 1);
return true;
},
getUser: function(id) {
return users.find(u => u.id === id) || null;
},
getAllUsers: function() {
return [...users]; // Return a copy to prevent mutation
},
getCount: function() {
return users.length;
}
};
})();
The returned object exposes five methods while users, maxUsers, validateUser, and generateId stay completely private. No outside code can read users directly or call validateUser — the only way in is through the public API. The getAllUsers method also returns a copy of the array to prevent external mutation of the internal state.
Here is how you use the module:
// Usage
UserManager.addUser({ name: "Alice" });
UserManager.addUser({ name: "Bob" });
console.log(UserManager.getCount()); // 2
console.log(UserManager.getAllUsers());
// [{ name: "Alice", id: "abc123" }, { name: "Bob", id: "def456" }]
// These are private and can't be accessed:
// console.log(users);
// console.log(validateUser());
Namespacing with Modules
One of the main benefits of the module pattern is creating namespaces to avoid global pollution. Instead of adding dozens of functions to the global scope, you group related functionality under a single object. This was the standard way to structure JavaScript applications before ES modules arrived. Libraries like jQuery used this exact pattern — a single $ variable that exposed everything through one namespace:
// Instead of polluting the global scope:
function formatCurrency() { }
function formatDate() { }
function formatNumber() { }
// You create a namespace:
const Formatter = (function() {
function currency(amount, locale = "en-US", currency = "USD") {
return new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(amount);
}
function date(dateObj, locale = "en-US") {
return new Intl.DateTimeFormat(locale).format(dateObj);
}
function number(num, decimals = 2) {
return num.toFixed(decimals);
}
return { currency, date, number };
})();
console.log(Formatter.currency(1234.56)); // $1,234.56
console.log(Formatter.date(new Date())); // 3/17/2026
console.log(Formatter.number(42.567, 1)); // 42.6
The Formatter module groups three related utilities under one name. Without it, each function would occupy the global scope and risk colliding with other libraries. This namespacing approach was critical before ES modules existed — it let developers structure large codebases without a bundler.
How the module pattern compares to ES modules
Modern JavaScript uses ES6 modules (import/export), which offer similar benefits but with better tooling and syntax. Here’s how they compare:
// ES6 Module (modern.js)
const privateVariable = "hidden";
export function publicFunction() {
return privateVariable;
}
export default { publicFunction };
// The module pattern (legacy-module.js)
const LegacyModule = (function() {
const privateVariable = "hidden";
function publicFunction() {
return privateVariable;
}
return { publicFunction };
})();
| Feature | Module Pattern | ES6 Modules |
|---|---|---|
| Syntax | Function-based | import/export keywords |
| Static analysis | Limited | Full (tree shaking) |
| Async loading | Manual | Native support |
| Circular dependencies | Error-prone | Supported |
| Private fields | Convention (#) | Truly private |
When to use each
The module pattern is still useful in certain scenarios:
- Single-file encapsulation: when you need to isolate code in a single file without setting up a module bundler
- Legacy browser support: for environments that don’t support ES6 modules
- Dynamic module creation: when you need to create modules conditionally at runtime
- Learning JavaScript: understanding the module pattern helps you understand how JavaScript scoping works
For new projects, prefer ES6 modules unless you have a specific reason not to.
Summary
The module pattern is a powerful tool for JavaScript developers:
- Uses IIFEs and closures to create private state
- The revealing module pattern makes public APIs explicit
- Namespacing reduces global pollution
- While ES6 modules are now preferred, the module pattern remains useful in specific scenarios
Understanding this pattern gives you insight into JavaScript’s scoping mechanics and helps you write more organized, encapsulated code.
Keeping modules small
The module pattern works best when each module has a narrow job. One module can manage a small piece of state, another can format values, and another can coordinate a handful of related actions. When a module becomes too large, the private state that once felt helpful can start to hide unrelated behavior. A smaller boundary makes the code easier to scan and easier to move.
That discipline also improves communication inside the team. A module with a clear name and a clear export list gives readers a quick summary of what the code is for. If a teammate can understand the module from its public surface, they do not need to dig through every private helper just to use it safely.
Testing module state
Private state can make tests cleaner when the public API is shaped well. The test only needs to exercise the exported methods and verify the result. That means the state details stay inside the module instead of leaking into the test setup. The smaller the public surface, the less fragile the tests usually are.
It still helps to watch for hidden coupling. If several exported methods depend on one shared array or counter, tests may need careful setup and teardown so they do not influence each other. Keeping module state predictable makes both runtime behavior and testing behavior easier to trust.
When to move on to ES modules
For new projects, ES modules usually make more sense because the syntax is direct and the tool support is strong. The module pattern still matters because it explains why encapsulation works and how closures can protect state. That knowledge is useful even when you no longer ship code in the older style.
The main question is not whether the module pattern is old. The question is whether it still solves a real problem for the code in front of you. If you need a quick closure-based boundary in a small script, it can still be a fine choice. If the project already has a module loader, standard imports are usually the cleaner path.
Module boundaries in practice
Good module boundaries are easy to describe. A module should expose the actions that belong together and hide the internal helpers that only support those actions. If you cannot describe the public surface in one or two short sentences, the module may be trying to do too much.
That description also helps when you revisit old code. You can quickly tell whether a new feature belongs inside an existing module or deserves a new one. Clear boundaries keep the codebase from turning into a pile of unrelated functions that happen to live in the same file.
Working in Layers
Modules become easier to trust when each layer has a different job. One layer can hold the private state, another can expose the public actions, and a third can connect the module to the rest of the app. That split keeps the responsibilities visible and reduces the chance that one helper starts doing everything.
Next steps
- Factory Pattern: factories often use the module pattern internally for private state
- Classes in JavaScript: compare class-based encapsulation with the closure-based approach shown here
See Also
- Functions and Scope: closures and scope, the foundation of the module pattern
- Modules and Imports: ES6 modules and modern JavaScript module systems
- JavaScript Modules: ESM: ES modules syntax and usage in depth