The original pattern, briefly
const Counter = (function () {
let count = 0;
return {
increment: () => ++count,
value: () => count,
};
})();
The IIFE creates a private scope. count is inaccessible from outside. Only the returned object is public. In 2026 you rarely write this, but the idea — hide state behind a small public surface — is what good modules still do.
What changed with ES modules
// counter.js
let count = 0;
export const increment = () => ++count;
export const value = () => count;
Everything not exported is private. The module itself is the closure. No wrapping function needed.
Where the pattern still earns its keep
Inside a module you often want multiple independent instances with private state. A bare ES module gives you one singleton. For instances, return a factory:
// counter.js
export function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
value: () => count,
};
}
This is the module pattern, alive and well — just scoped to the function rather than the file. Each call gets its own count.
Why not a class?
Classes give you the same thing with different ergonomics:
export class Counter {
#count = 0;
increment() { return ++this.#count; }
value() { return this.#count; }
}
Private fields (#count) now enforce encapsulation at runtime. For most teams, classes are fine. I reach for factory functions when I want structural typing (anything shaped like { increment, value } works), or when I want to return a subset of the closure based on construction arguments.
The trap I watched a team fall into
They wrote a module with top-level state:
let config = null;
export function init(c) { config = c; }
export function getFlag(name) { return config.flags[name]; }
Works fine until two tests import it. Test A calls init({...}). Test B reads stale state. Module singletons are global state with extra steps — and they bleed across test boundaries.
The fix is boring: pass config explicitly, or return a factory that closes over it.
The rule I follow
If a module exports only pure functions, top-level is fine. The moment there is mutable state, wrap it in a factory and let the caller decide how many instances exist. Future-you, writing a test at 11pm, will be grateful.
