Module Patterns before ES Modules
IIFE, Revealing Module, AMD, CommonJS, UMD — how pre-ESM isolation worked and what still shows up in legacy bundles.
- javascript
- module-patterns
Before import/export shipped everywhere, JS had one shared global scope in browsers. Names collided. Teams invented patterns to fake privacy and dependency graphs. You still read them in older libraries and interview “how would you isolate state?” questions.
The problem modules solve
- Avoid polluting
window - Hide private helpers
- Declare dependencies explicitly
- Load code in a controlled order
ES modules do this natively. Pre-ESM patterns approximate it with closures and conventions.
IIFE — instant privacy
var counter = (function () {
var n = 0; // private
return {
inc() { return ++n; },
value() { return n; },
};
})();
counter.inc(); // 1
// no access to n from outside
An Immediately Invoked Function Expression creates a scope, runs once, returns a public API. This is the ancestor of the Revealing Module pattern.
Revealing Module
Same idea, named for returning only the methods you “reveal”:
var cart = (function () {
var items = [];
function add(item) { items.push(item); }
function total() {
return items.reduce((s, i) => s + i.price, 0);
}
return { add: add, total: total };
})();
Private state lives in the closure. Public surface is the returned object. No real privacy against determined callers who monkey-patch the returned methods’ closures… wait — they can’t reach items unless you leak it. Good enough for app code.
CommonJS (Node-style)
// math.js
const PI = 3.14159;
function area(r) { return PI * r * r; }
module.exports = { area };
// app.js
const { area } = require('./math');
- Sync
require module.exportsis the export object- One file, one module instance (cache)
Bundlers (webpack, Browserify) rewrote this for the browser by wrapping each file in a function and shipping a runtime require.
AMD (Asynchronous Module Definition)
// define(deps, factory)
define(['./math'], function (math) {
return {
circle(r) { return math.area(r); },
};
});
Designed for browsers: load deps async, then run factory. RequireJS was the main loader. Verbose; mostly historical now.
UMD — “works everywhere”
A UMD wrapper detects the environment:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports'], factory);
} else if (typeof module === 'object' && module.exports) {
factory(module.exports);
} else {
factory((root.myLib = {}));
}
})(typeof self !== 'undefined' ? self : this, function (exports) {
exports.hello = () => 'hi';
});
Still appears in some CDN builds of dual-package libraries.
Comparison
| Pattern | Load | Privacy | Today |
|---|---|---|---|
| Global + IIFE | script tags | Closure | Tiny embeds |
| CommonJS | sync require | file scope | Node, some bundles |
| AMD | async define | factory scope | Legacy |
| UMD | hybrid | factory | CDN dual builds |
| ESM | static + dynamic import | module scope | Default |
What interviewers care about
- Why globals are bad (collision, testing, tree-shaking)
- How a closure gives “private” fields without
#or ESM - That CJS is live-binding-free for the export object properties you mutate vs ESM live bindings
- Circular deps behave differently in CJS vs ESM
// CJS: partial exports during cycle are common footgun
// ESM: live bindings; TDZ if you read before init
Interview answer (out loud)
“Before ESM, we used IIFEs and the revealing module pattern for private state, CommonJS for Node sync modules, and AMD/UMD for browser loaders. They’re all conventions built on functions and objects. ES modules give real isolation, static analysis, and
import/exportsyntax — use those for new code, but recognize IIFE/CJS in legacy codebases.”
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.