Module Pattern: Creating Encapsulation with Closures
The Module Pattern leverages closures to create private and public interfaces, enabling data encapsulation and namespace management. It's a powerful design pattern that forms the foundation of modern modular JavaScript development.
What is the Module Pattern?
The Module Pattern uses closures to create private scope and expose a selective public interface. It combines Immediately Invoked Function Expressions (IIFE) with closures to create self-contained modules with controlled access to internal state and functionality.
Basic Module Pattern
const Counter = (function() {
// Private variables
let count = 0;
// Private functions
function log(msg) {
console.log('[Counter] ' + msg);
}
// Public API (returned object)
return {
increment: function() {
count++;
log('Count is now: ' + count);
return count;
},
get: function() {
return count;
},
reset: function() {
count = 0;
log('Count reset');
}
};
})();
// Usage
Counter.increment(); // [Counter] Count is now: 1
Counter.increment(); // [Counter] Count is now: 2
console.log(Counter.get()); // 2
Counter.reset(); // [Counter] Count reset
// Private members are inaccessible
// console.log(Counter.count); // undefinedThe module creates a private scope with hidden count variable. Only methods in the returned object can access it.
Core Concepts
IIFE (Immediately Invoked Function Expression)
IIFE Creates Module Scope
// IIFE: Function defined and called immediately
(function() {
let privateVar = 'private';
console.log(privateVar); // 'private' - only accessible here
})();
// console.log(privateVar); // ReferenceError
// Module returns public interface
const Module = (function() {
let privateVar = 'private';
return {
getPrivate: function() {
return privateVar; // Closure accesses privateVar
}
};
})();
console.log(Module.getPrivate()); // 'private'IIFE creates a temporary scope. Returning an object creates closures that remember that scope.
Data Privacy and Encapsulation
Private Variables Cannot Be Modified Externally
const BankAccount = (function() {
let balance = 1000; // Truly private, cannot be accessed directly
return {
deposit: function(amount) {
if (amount > 0) {
balance += amount;
console.log('Deposited: ' + amount);
}
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
console.log('Withdrew: ' + amount);
}
},
getBalance: function() {
return balance;
}
};
})();
BankAccount.deposit(500); // Deposited: 500
BankAccount.withdraw(200); // Withdrew: 200
console.log(BankAccount.getBalance()); // 1300
// Cannot hack the account
// BankAccount.balance = 1000000; // Doesn't work
// BankAccount.balance is undefinedPrivate variables like balance cannot be accessed or modified from outside. Only the public methods can modify them.
Module Pattern Variations
Revealing Module Pattern
Define Functions, Reveal Only Public
const Calculator = (function() {
// Private: Define all functions first
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
function validateNumbers(x, y) {
return typeof x === 'number' && typeof y === 'number';
}
// Public: Reveal only what's needed
return {
add: function(x, y) {
if (!validateNumbers(x, y)) throw new Error('Invalid input');
return add(x, y);
},
subtract: function(x, y) {
if (!validateNumbers(x, y)) throw new Error('Invalid input');
return subtract(x, y);
},
multiply: function(x, y) {
if (!validateNumbers(x, y)) throw new Error('Invalid input');
return multiply(x, y);
}
};
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
// Calculator.validateNumbers(5, 3); // TypeErrorAll functions are defined privately. The return statement selectively reveals only those needed in the public API.
Advanced Module Patterns
Singleton Pattern
Module Ensures Single Instance
const DatabaseConnection = (function() {
let instance = null;
function createConnection() {
return {
id: Math.random().toString(36).slice(2, 9),
connected: true,
query: function(sql) {
console.log('Executing: ' + sql);
return 'results';
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createConnection();
console.log('Connection created: ' + instance.id);
}
return instance;
}
};
})();
// Always returns same instance
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - Same object!The module pattern naturally creates singletons. Each module is instantiated once and reused.
Module with Dependencies
Passing Dependencies as Parameters
// User module
const UserModule = (function() {
let users = [];
return {
add: (name) => users.push(name),
getAll: () => users.slice(),
count: () => users.length
};
})();
// Authentication using UserModule dependency
const AuthModule = (function(userService) {
let authenticated = false;
const validUsers = ['admin', 'user'];
return {
login: function(name) {
if (validUsers.includes(name)) {
authenticated = true;
userService.add(name);
console.log(name + ' logged in');
}
},
isAuthenticated: function() {
return authenticated;
},
getLoggedInCount: function() {
return userService.count();
}
};
})(UserModule); // Pass dependency as parameter
// Usage
AuthModule.login('admin');
AuthModule.login('user');
console.log(AuthModule.getLoggedInCount()); // 2Modules can accept dependencies as parameters. This enables better testing and loose coupling.
Augmentation Pattern
Extending Existing Modules
// Base module
const StringUtils = (function() {
return {
trim: (str) => str.trim(),
toLowerCase: (str) => str.toLowerCase(),
toUpperCase: (str) => str.toUpperCase()
};
})();
// Augment the module without modifying original
(function(utils) {
utils.capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
utils.reverse = function(str) {
return str.split('').reverse().join('');
};
utils.isPalindrome = function(str) {
const clean = str.toLowerCase().replace(/\s/g, '');
return clean === utils.reverse(clean);
};
})(StringUtils);
// Usage
console.log(StringUtils.capitalize('hello')); // 'Hello'
console.log(StringUtils.reverse('world')); // 'dlrow'
console.log(StringUtils.isPalindrome('racecar')); // trueAugmentation allows extending modules without modifying original code. Create new features externally.
Common Module Pattern Mistakes
❌ Exposing Private Variables
Mistake: Accidentally Exposing Internals
// WRONG - Exposes internals
const BadModule = (function() {
let secretData = 'secret';
let internalState = {};
return {
secret: secretData, // Exposes private variable!
state: internalState, // Reference to internal object!
};
})();
// Can modify internals from outside
BadModule.secret = 'hacked';
BadModule.state.pwned = true;
// CORRECT - Control what's exposed
const GoodModule = (function() {
let secretData = 'secret';
return {
getSecret: function() {
return secretData; // Read-only access
},
getState: function() {
return { ...secretData }; // Return copy, not reference
}
};
})();Only expose methods, not variables. Return copies or read-only access to prevent external modification.
❌ Memory Leaks from Closures
Mistake: Holding References Too Long
// PROBLEM - Closure holds large data even when unused
const Module = (function() {
let largeArray = new Array(1000000).fill('data'); // Large object
return {
getValue: function() {
return 42; // Doesn't use largeArray, but it's still held!
},
cleanup: function() {
// Cleanup not provided - no way to free memory
}
};
})();
// SOLUTION - Provide cleanup and proper scoping
const ModuleFixed = (function() {
let largeData = null;
return {
load: function() {
largeData = new Array(1000000).fill('data');
},
getValue: function() {
return 42;
},
unload: function() {
largeData = null; // Free memory
}
};
})();Closures capture their entire scope. Provide cleanup methods to free unused variables.
❌ Overcomplicating Simple Code
Mistake: Using Modules for Simple Functions
// OVERCOMPLICATION - Simple function doesn't need Module Pattern
const MathModule = (function() {
return {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
})();
// BETTER - Keep it simple for simple cases
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// Use Module Pattern ONLY for:
// - Data privacy needs
// - Complex state management
// - Namespace organization
// - Preventing global pollutionUse Module Pattern when you need encapsulation. For simple utilities, keep it straightforward.
Module Pattern Best Practices
- ✓Use IIFE to create module scope immediately
- ✓Keep private variables and functions hidden - return only what's needed
- ✓Use Revealing Module Pattern for clarity - define all functions, reveal selectively
- ✓Accept dependencies as parameters for testability
- ✓Provide cleanup methods if module holds large resources
- ✓Use for complex state or when standard functions don't suffice
Module Pattern vs ES6 Modules
| Feature | Module Pattern | ES6 Modules |
|---|---|---|
| Syntax | IIFE (function) | import/export keywords |
| Encapsulation | Closures | Language level |
| Static Analysis | Not possible | Full support |
| Tree Shaking | Not supported | Full support |
| Browser Support | All versions | Modern browsers |
Related Topics
Frequently Asked Questions
When should I use the Module Pattern?
Use it when you need data privacy, complex state management, or organizing code into namespaces. For simple utilities, ES6 modules are often better.
What's the difference between Module Pattern and ES6 modules?
Module Pattern uses closures and IIFEs. ES6 modules use native language features with import/export, better tree-shaking, and static analysis support.
Can I modify private variables from outside a module?
No, truly private variables cannot be accessed externally. Only public methods can access and modify them. This is the power of the Module Pattern.
How do I test private functions?
Private functions are tested indirectly through the public API. If you need direct testing, consider exposing them or using a different architecture.
What about memory leaks with Module Pattern?
Modules hold their entire closure scope. If they reference large objects, that memory cannot be garbage collected. Provide cleanup methods when needed.
Can modules have multiple instances?
Yes. By default, modules are singletons. To create multiple instances, wrap the module in a factory function.