Module Pattern
Encapsulate private state and behavior inside a self-contained unit with a public API. A structural pattern for organizing code into reusable, scope-safe modules.
Overview
The Module Pattern keeps private state and helper functions inside a self-contained unit and exposes only the API you choose. Nothing leaks into the global namespace, so unrelated parts of a codebase can’t collide or reach into each other’s internals.
Before ES6, JavaScript achieved this with Immediately Invoked Function Expressions — wrap everything in a function, call it, return the public bits. Modern languages ship native modules instead (ES modules, Python packages, Java packages), but the core idea is the same either way: hide internals, publish an interface.
When to Use
- You need private state that can’t be reached from outside the unit
- Components sharing a codebase must not collide on names
- You want consumers to see a clean API while implementation details stay hidden
- Tests need clear boundaries between the public contract and private helpers
If you need to wrap an existing class behind a different interface rather than encapsulate state, the Adapter Pattern is the one you’re after.
When to Avoid
- The language gives you native modules with real encapsulation — use those first
- A simple script that gains nothing but indirection from being wrapped
- Modules that would depend on each other deeply enough to create circular imports
Solution
JavaScript (IIFE)
const CounterModule = (function () {
let count = 0; // Private state — unreachable from outside
function log(operation) {
console.log(`Counter ${operation}: ${count}`);
}
return {
increment() {
count++;
log('incremented');
return count;
},
decrement() {
count--;
log('decremented');
return count;
},
getCount() {
return count;
}
};
})();
// Usage
CounterModule.increment();
CounterModule.increment();
console.log(CounterModule.getCount()); // 2
// CounterModule.count is undefined — not accessible
JavaScript (ES Module)
// counter.js
let count = 0; // Module-scoped — not exported, not accessible
function log(operation) {
console.log(`Counter ${operation}: ${count}`);
}
export function increment() {
count++;
log('incremented');
return count;
}
export function decrement() {
count--;
log('decremented');
return count;
}
export function getCount() {
return count;
}
// main.js
import { increment, getCount } from './counter.js';
increment();
console.log(getCount());
Python
# counter.py
_count = 0 # Module-private by convention
def _log(operation):
print(f"Counter {operation}: {_count}")
def increment():
global _count
_count += 1
_log("incremented")
return _count
def decrement():
global _count
_count -= 1
_log("decremented")
return _count
def get_count():
return _count
# main.py
from counter import increment, get_count
increment()
increment()
print(get_count()) # 2
# counter._count is accessible but discouraged — leading underscore marks intent
Java
// com.myapp.counter.Counter.java
package com.myapp.counter;
public class Counter {
private static int count = 0;
private static void log(String operation) {
System.out.println("Counter " + operation + ": " + count);
}
public static int increment() {
count++;
log("incremented");
return count;
}
public static int decrement() {
count--;
log("decremented");
return count;
}
public static int getCount() {
return count;
}
}
// Usage
Counter.increment();
System.out.println(Counter.getCount());
Explanation
The mechanism differs per language, but the contract is identical. In an IIFE, the returned object is the only thing that escapes — count and log live inside the closure, and the JavaScript engine keeps them alive only because the returned methods still reference them. There’s no syntax for reaching in from outside; privacy isn’t a convention, it’s enforced by scope.
ES modules do the same thing statically. Top-level declarations that aren’t exported are invisible to importers — count and log exist in the module’s scope and nowhere else. The difference matters at build time: a bundler can analyze import/export statements without executing anything, which is what makes tree-shaking possible (the bundle size optimization guide goes deep on why that matters for shipped code). An IIFE is opaque to that analysis — the bundler can’t tell which parts are used, so everything ships.
Python takes the weakest form: import gives real module scoping, but _count is only private by convention — anyone can still reach counter._count if they insist. Java sits between the two: private members are truly inaccessible from other classes, but “module” in the Java sense (a package, or a JPMS module since Java 9) is about grouping classes more than hiding values.
One consequence worth understanding: module state is shared state. Every importer of counter.js gets the same count — the module is a singleton namespace, whether you meant it or not. When you need several independent counters, you need a factory or a class, not a module. (And if you’re tempted to keep one global module per app state slice, the Singleton Pattern has the same shape with the same traps.)
Variants
| Variant | Mechanism | Use Case |
|---|---|---|
| IIFE Module | Closure-based privacy | Pre-ES6 JavaScript, inline scripts |
| Revealing Module | Returns an object literal mapping public names to private functions | Cleaner API definition in JS |
| CommonJS | module.exports / require | Node.js before ES modules, some tooling |
| ES Module | export/import | Modern JavaScript standard |
| Namespace Module | Object literal namespace | Grouping utilities under one global |
What Works
- One concern per module. A module that counts, formats dates, and makes HTTP requests is three modules wearing a trench coat.
- Export explicitly. Wildcard exports hide the contract — a reader should see the public API at a glance.
- Name private members clearly. Python’s
_prefixsays “internal, don’t touch” without enforcement; JavaScript closures don’t need the hint. - Kill circular dependencies early. A imports B, B imports A — extract the shared part into a third module before the cycle ossifies.
- Keep modules small. Past ~200 lines, a module usually wants splitting; past 500, it definitely does.
Common Mistakes
- Global state in modules makes them untestable. A counter module with shared
countcan’t be reset between tests — you’ll end up with order-dependent test suites. export *wildcard exports leak every internal helper as an accidental public API. The moment someone imports it, you’re stuck maintaining it.- Side effects on import — connecting to a database, registering listeners, mutating globals at load time — make modules unpredictable and break test isolation.
- Deeply nested paths (
a.b.c.d.e) create brittle import chains that snap on every refactor. Keep the graph shallow. - Treating modules as classes. A module is a namespace, not a blueprint — there’s no
new CounterModule(). When you need instances, write a factory or a class.
Real-World Examples
Node.js Core Modules
fs, path, and http wrap OS operations behind stable APIs. The internal C++ bindings stay private — you never touch them directly, which is exactly the point.
Python Standard Library
json, re, and urllib hide parsers and regex compilers behind a handful of functions. json.loads is a public function; the scanner underneath is module-private.
Angular Modules
NgModule groups components, services, and directives into feature units with explicit exports — the framework’s own take on “hide internals, publish an interface.”
Troubleshooting
- Circular imports at runtime: A imports B, B imports A — Node gives you a partially-initialized module (
undefinedexports where you expected functions). Extract the shared dependency into a third module both sides import. - State leaks between tests: module state persists for the whole test process. Reset it explicitly in setup, or inject a factory so each test builds a fresh instance.
requireof an ES module fails: Node won’trequire()an.mjsfile or an"type": "module"package on older versions — switch toimportor pin to a version whererequire(esm)is supported.- Import works locally but breaks in the bundler: usually a case-sensitivity mismatch (
./Counter.jsvs./counter.js) — Linux CI will find it even if macOS doesn’t. - Everything is exported “just in case”: a bloated public surface becomes maintenance debt. Export what consumers need today; add the rest when asked.
Advanced Topics
Scenario: Module Pattern for Encapsulation in JS
// Module pattern: private encapsulation with closures
const PaymentModule = (function() {
// Private: not accessible from outside
let apiKey = "";
let transactions: Transaction[] = [];
const MAX_TRANSACTIONS = 1000;
// Private: validate API key
function validateKey(key: string): boolean {
return key.startsWith("pk_") && key.length === 32;
}
// Private: internal logging
function log(msg: string) {
console.log(`[PAYMENT] ${new Date().toISOString()} ${msg}`);
}
// Public: exposed API
return {
init(key: string) {
if (!validateKey(key)) throw new Error("Invalid API key");
apiKey = key;
log("Module initialized");
},
charge(amount: number, currency: string): Promise<Transaction> {
if (!apiKey) throw new Error("Module not initialized");
if (transactions.length >= MAX_TRANSACTIONS) {
throw new Error("Transaction limit reached");
}
log(`Charging ${amount} ${currency}`);
const tx = { id: crypto.randomUUID(), amount, currency, status: "pending" };
transactions.push(tx);
return Promise.resolve(tx);
},
getTransactions(): Transaction[] { return [...transactions]; },
getStats() {
return {
total: transactions.length,
pending: transactions.filter(t => t.status === "pending").length,
completed: transactions.filter(t => t.status === "completed").length,
};
}
};
})();
// Usage
PaymentModule.init("pk_test_1234567890123456789012345678");
const tx = await PaymentModule.charge(99.99, "USD");
console.log(PaymentModule.getStats()); // { total: 1, pending: 1, completed: 0 }
// PaymentModule.apiKey -> undefined (private)
// PaymentModule.validateKey -> undefined (private)
This example shows why the pattern survived ES6: init creates a lifecycle the caller controls, apiKey and transactions are unreachable without reflection tricks, and the cap on MAX_TRANSACTIONS shows state enforcement that would be awkward to bolt onto a plain object. The trade-off is the one mentioned earlier — everything ships, always, because the bundler can’t see inside the closure.
Further Reading
- MDN — JavaScript modules — the definitive reference for
import/exportsemantics - Companion resources — runnable IIFE, ES module, Python and Java examples plus the payment scenario
Frequently Asked Questions
Is the Module Pattern the same as a class?
No. A class is a blueprint you instantiate many times; a module is a single namespace evaluated once. Use modules to organize code and classes when you need several instances with their own state.
Can modules depend on each other?
Yes, that's normal — just avoid cycles. If A imports B and B imports A, extract the shared piece into a third module both depend on.
Should I still use the IIFE Module Pattern in 2026?
Only for legacy code or inline scripts without a build step. ES modules are the standard — native in browsers and Node, statically analyzable, tree-shakeable. An IIFE ships everything; an ES module lets the bundler drop what you don't import.
How do I migrate an IIFE module to an ES module?
Move the file's contents to top level, delete the wrapper function, and replace the returned object with individual export statements. Private members stay unexported — they were private by scope before, and they're private by non-export now. The behavior is identical; the syntax is cleaner.
How do I test module-private functions?
You don't — test them through the public API. If a private function is complex enough to need direct tests, that's a sign it belongs in its own module.
What's the difference between a module and a singleton?
A module is a file-level namespace; a singleton is a deliberately single instance of a class or object. Every ES module is a singleton in practice, but singletons can also be created lazily and injected — modules can't be swapped that way.
Related Resources
Facade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternSingleton Pattern: Java, Python & TypeScript Examples
Ensure a class has only one instance and provide global access to it — the creational design pattern for controlled object creation, with production-ready implementations.
PatternRepository Pattern
Abstract data access logic behind a clean interface. An architectural design pattern for testable, maintainable data layers.
PatternPlugin Pattern
Enable third-party extensions by defining extension points in a host system that loads and executes live external modules.
GuideBundle Size Optimization: A Practical Frontend Guide
A hands-on guide to measuring and reducing JavaScript bundle size with tree shaking, code splitting, dependency swaps, compression, and CI budgets.