JavaScript Module Pattern: Advanced Tutorial
Master the JavaScript module pattern with advanced techniques. Covers IIFE modules, private state encapsulation, augmentation patterns, loose and tight augmentation, sub-modules, cross-file modules, and comparing module patterns with ES modules.
The module pattern uses closures and IIFEs to create private scope, encapsulating implementation details while exposing a clean public API. Despite ES modules being the modern standard, understanding this pattern is essential for maintaining legacy code and grasping JavaScript scope mechanics.
For the revealing variant, see Implementing the Revealing Module Pattern JS.
Basic Module Pattern
// The module pattern uses an IIFE to create private scope
const CounterModule = (function () {
// Private state - not accessible from outside
let count = 0;
const MAX_COUNT = 100;
// Private function
function validate(value) {
if (value < 0) throw new Error("Count cannot be negative");
if (value > MAX_COUNT) throw new Error(`Count cannot exceed ${MAX_COUNT}`);
return true;
}
// Public API (returned object)
return {
increment() {
const newCount = count + 1;
validate(newCount);
count = newCount;
return count;
},
decrement() {
const newCount = count - 1;
validate(newCount);
count = newCount;
return count;
},
getCount() {
return count;
},
reset() {
count = 0;
return count;
},
};
})();
console.log(CounterModule.increment()); // 1
console.log(CounterModule.increment()); // 2
console.log(CounterModule.getCount()); // 2
// CounterModule.count -> undefined (private)
// CounterModule.validate -> undefined (private)Module with Dependencies
// Pass dependencies as IIFE arguments for explicit wiring
const UserService = (function (storage, validator, eventBus) {
const STORAGE_KEY = "users";
let cache = null;
function loadFromStorage() {
if (cache) return cache;
const data = storage.get(STORAGE_KEY);
cache = data ? JSON.parse(data) : [];
return cache;
}
function saveToStorage(users) {
cache = users;
storage.set(STORAGE_KEY, JSON.stringify(users));
}
return {
getAll() {
return [...loadFromStorage()];
},
getById(id) {
return loadFromStorage().find((u) => u.id === id) || null;
},
create(userData) {
const errors = validator.validate(userData, {
name: { required: true, minLength: 2 },
email: { required: true, pattern: /^[^@]+@[^@]+$/ },
});
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(", ")}`);
}
const users = loadFromStorage();
const newUser = {
id: Date.now().toString(36),
...userData,
createdAt: new Date().toISOString(),
};
users.push(newUser);
saveToStorage(users);
eventBus.emit("user:created", newUser);
return newUser;
},
delete(id) {
const users = loadFromStorage().filter((u) => u.id !== id);
saveToStorage(users);
eventBus.emit("user:deleted", { id });
},
clearCache() {
cache = null;
},
};
})(StorageModule, ValidatorModule, EventBusModule);Module Augmentation
// Module augmentation adds functionality to an existing module
// Useful for splitting large modules across files
// File 1: core.js
var AppModule = (function (mod) {
mod.version = "1.0.0";
mod.init = function (config) {
mod._config = config;
console.log("App initialized with:", config);
};
return mod;
})(AppModule || {});
// File 2: utils.js (augments AppModule)
var AppModule = (function (mod) {
mod.utils = {
formatDate(date) {
return new Date(date).toLocaleDateString();
},
generateId() {
return Math.random().toString(36).slice(2, 11);
},
debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
};
return mod;
})(AppModule || {});
// File 3: api.js (augments AppModule)
var AppModule = (function (mod) {
const baseUrl = "/api";
mod.api = {
async get(endpoint) {
const response = await fetch(`${baseUrl}${endpoint}`);
return response.json();
},
async post(endpoint, data) {
const response = await fetch(`${baseUrl}${endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
},
};
return mod;
})(AppModule || {});
// All three files contribute to the same module
// AppModule.init({ debug: true });
// AppModule.utils.formatDate(Date.now());
// AppModule.api.get("/users");Tight vs Loose Augmentation
// LOOSE AUGMENTATION: files can load in any order
// Uses (AppModule || {}) so module is created if it does not exist yet
var AppModule = (function (mod) {
mod.featureA = function () { return "A"; };
return mod;
})(AppModule || {});
// TIGHT AUGMENTATION: requires specific load order
// Module MUST already exist (no || {} fallback)
var AppModule = (function (mod) {
// Can override or extend existing methods
const originalInit = mod.init;
mod.init = function (config) {
console.log("Pre-init hook");
originalInit(config);
console.log("Post-init hook");
};
return mod;
})(AppModule); // Will throw if AppModule does not exist
// CHOOSING BETWEEN THEM:
// Loose: independent features that don't depend on each other
// Tight: extensions that override or depend on existing module members| Pattern | Load Order | Module Must Exist | Use Case |
|---|---|---|---|
| Loose augmentation | Any order | No | Independent features |
| Tight augmentation | Specific order | Yes | Overrides and extensions |
| Sub-modules | After parent | Yes | Namespace organization |
| Dependency injection | IIFE arguments | Dependencies first | Testable modules |
Module Factory
// Factory function creates configurable module instances
function createStore(initialState = {}) {
let state = { ...initialState };
const listeners = new Set();
const middleware = [];
function notify() {
const currentState = getState();
listeners.forEach((listener) => listener(currentState));
}
function getState() {
return { ...state };
}
function setState(updater) {
const prevState = { ...state };
const updates = typeof updater === "function"
? updater(prevState)
: updater;
// Run middleware
let finalUpdates = updates;
for (const mw of middleware) {
finalUpdates = mw(prevState, finalUpdates) || finalUpdates;
}
state = { ...state, ...finalUpdates };
notify();
return getState();
}
function subscribe(listener) {
listeners.add(listener);
return function unsubscribe() {
listeners.delete(listener);
};
}
function use(mw) {
middleware.push(mw);
}
return { getState, setState, subscribe, use };
}
// Each call creates an independent instance
const userStore = createStore({ users: [], loading: false });
const cartStore = createStore({ items: [], total: 0 });
userStore.subscribe((state) => console.log("Users:", state.users.length));
userStore.setState({ users: [{ name: "Alice" }], loading: false });Module Pattern vs ES Modules
// MODULE PATTERN (pre-ES6)
const MyModule = (function () {
let _private = 0;
return {
get() { return _private; },
set(v) { _private = v; },
};
})();
// ES MODULE (modern)
// myModule.js
let _private = 0;
export function get() { return _private; }
export function set(v) { _private = v; }
// Comparison:
// Module Pattern:
// + Works in all environments
// + No build tools required
// + Immediate execution
// - Global namespace pollution
// - No static analysis for tree shaking
// - Manual dependency management
// ES Modules:
// + Native browser support
// + Static analysis (tree shaking)
// + Clear dependency graph (import/export)
// + Scoped by default (no global pollution)
// - Requires build tools for older browsers
// - Asynchronous loading semanticsRune AI
Key Insights
- IIFEs create true private scope: Variables declared inside the IIFE are inaccessible from outside, providing genuine encapsulation through closures
- Dependency injection via IIFE arguments: Pass dependencies as arguments to make modules testable and loosely coupled
- Loose augmentation allows any load order: Using
(Module || {})lets files extend a module independently without requiring a specific script order - Module factories create independent instances: Factory functions return new module objects each time, unlike the singleton IIFE pattern
- ES modules are the modern replacement: For new code, use native import/export syntax which provides static analysis, tree shaking, and browser-native loading
Frequently Asked Questions
When should I still use the module pattern over ES modules?
How does the module pattern achieve private variables?
Can I unit test private functions in a module pattern?
What is the difference between module pattern and namespace pattern?
How do I handle circular dependencies with the module pattern?
Conclusion
The module pattern remains foundational JavaScript knowledge for understanding closures, encapsulation, and scope. Module factories create configurable instances, while augmentation patterns split large modules across files. For the cleaner revealing variant, see Implementing the Revealing Module Pattern JS. For the singleton pattern built on modules, see JavaScript Singleton Pattern: Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.