RizTech Academy logo
RizTech Academy
Design Patterns in JavaScriptLesson 2 of 630 min

The module pattern, and what closures replaced

This is the pattern JavaScript invented for itself, and the one that explains why the language looked the way it did for fifteen years.

The problem it solved

Before 2015 JavaScript had no modules. Every <script> shared one global scope:

<script src="orders.js"></script>
<script src="rates.js"></script>
// orders.js
let state = [];          // global
function render() { … }  // global

// rates.js
let state = {};          // silently replaces the other one

Two files, one namespace, and the second state wins. On a page with five scripts and a jQuery plugin, this was a genuine, daily source of bugs.

The module pattern

The fix was an immediately invoked function expression — an IIFE:

const orderStore = (function () {
  // Private. Nothing outside this function can reach them.
  let orders = [];
  const listeners = new Set();

  function notify() {
    for (const listener of listeners) listener();
  }

  // Public. Only what is returned escapes.
  return {
    add(order) {
      orders.push(order);
      notify();
    },
    all() {
      return [...orders];    // a copy, so callers cannot push into ours
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
})();
orderStore.add(order);
orderStore.all();
orderStore.orders;       // undefined — genuinely inaccessible

One global instead of four, and orders is actually private. Not private by convention like an underscore — there is no expression you can write that reaches it.

That is closures doing the work. The returned functions keep a reference to the scope they were created in, so orders stays alive after the IIFE has finished, reachable only by them.

ES modules made it a language feature

// store.js
let orders = [];
const listeners = new Set();

export function add(order) {
  orders.push(order);
  notify();
}

export function all() {
  return [...orders];
}
import { add, all } from "./store.js";

Same result — orders is unreachable from outside, because a module's top-level scope is private unless exported. The IIFE is gone because the module boundary does that job now.

Your capstone's state.js is exactly this, and it is worth re-reading with the pattern in mind: module-level let orders, a Set of listeners, and a handful of exported functions. It is the module pattern with the ceremony removed.

Modules are singletons

A subtlety that catches people, and the reason the next lesson can be short:

// store.js
console.log("store.js evaluating");
let orders = [];
export function add(o) { orders.push(o); }
export function count() { return orders.length; }
// a.js
import { add } from "./store.js";
add({ id: 1 });

// b.js
import { count } from "./store.js";
console.log(count());   // 1 — the same module, the same array

"store.js evaluating" prints once, however many files import it. A module is evaluated the first time it is imported and cached; every later import gets the same instance.

So export const config = … is a singleton, with none of the machinery other languages need — no private constructor, no getInstance, no double-checked locking. It is the most common reason a JavaScript codebase does not have a singleton pattern in it.

And it carries the same warning: a module holding mutable state is global mutable state. Fine for a store deliberately shared by the whole app. Not fine as a habit, and the reason your capstone's state module hands out copies rather than the array itself.

Closures beyond modules

The same mechanism is behind several things you have already used.

A counter that cannot be tampered with

function makeCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const c = makeCounter();
c.increment();
c.value();      // 1
c.count;        // undefined

Configuration captured once

function makeFormatter(currency) {
  const nf = new Intl.NumberFormat("en-IN", { style: "currency", currency });
  return (paise) => nf.format(paise / 100);
}

const formatRupees = makeFormatter("INR");
formatRupees(28_500);   // "₹285.00"

The Intl.NumberFormat is built once and reused by every call — which matters, because constructing one is surprisingly expensive.

The loop bug, and why let fixed it

// var: one binding, shared by all three
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

// let: a new binding per iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2

This is the single most-asked JavaScript interview question, and the answer is about closures: with var there is one i for the whole loop and all three callbacks close over it. With let the language creates a fresh binding each iteration, so each callback closes over its own.

Before let, the fix was an IIFE per iteration — which is the module pattern again, used to manufacture a scope the language would not give you.

When to use which now

Want Use
Private state across a file an ES module
Private state per instance a closure, or a #private class field
One shared instance a module export
Several independent instances a factory returning a closure

The IIFE is history. You will meet it in older code and in bundled output, and you should recognise it — but write an ES module.

Check your work

What the module pattern solved: every script sharing one global scope.

How the privacy works: returned functions close over the IIFE's scope, so the variables stay alive and unreachable.

Why ES modules replaced it: a module's top level is private unless exported.

Why modules are singletons: a module is evaluated once on first import and cached.

What that means for export const: it is a singleton with no machinery — and carries the same warning about global mutable state.

Why var in a loop gives 3, 3, 3: one binding shared by every callback; let creates a fresh binding per iteration.

What the old per-iteration IIFE was doing: manufacturing a scope the language would not provide.

When a closure still beats a module: per-instance private state.

Practice

  1. Write the orderStore IIFE above and try to reach orders from outside.
  2. Rewrite it as an ES module and confirm the same privacy.
  3. Put a console.log at the top of a module and import it from three files. Count how many times it prints.
  4. Import a counter module from two files, increment in one, read in the other.
  5. Open your capstone's state.js and identify the private state and the public surface.
  6. Change all() to return orders directly instead of a copy, then push into it from outside.
  7. Write makeCounter and create two independent counters.
  8. Run both loop examples and explain the difference without using the word "scope".
  9. Rewrite the var version to print 0, 1, 2 using an IIFE instead of let.
  10. Find an IIFE in a bundled file from any website's dev tools.

Next: factories, and why JavaScript rarely needs a builder.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship