The Module Pattern in JavaScript

· 5 min read · Updated March 17, 2026 · intermediate
design-patterns modules encapsulation javascript

The module pattern is one of the most useful design patterns in JavaScript. It provides a way to encapsulate code, create private state, and expose a clean public API. While ES6 modules have largely replaced the traditional module pattern, understanding it helps you appreciate modern JavaScript and gives you tools for scenarios where you need more control over encapsulation.

What Is the Module Pattern?

The module pattern uses closures to create private state that cannot be accessed from outside the module. It combines IIFEs (Immediately Invoked Function Expressions) with closures to achieve encapsulation. The key idea is simple: wrap your code in a function, create variables and functions inside that function, and return only what you want to be public.

const MyModule = (function() {
  // Private variables and functions
  let privateCounter = 0;
  
  function privateFunction() {
    return "This is private";
  }
  
  // Public API
  return {
    publicMethod: function() {
      privateCounter++;
      return privateFunction();
    },
    getCounter: function() {
      return privateCounter;
    }
  };
})();

console.log(MyModule.publicMethod()); // This is private
console.log(MyModule.getCounter());  // 1

// These would be undefined or throw errors:
// console.log(MyModule.privateCounter);
// console.log(MyModule.privateFunction());

IIFE: The Foundation

An IIFE (Immediately Invoked Function Expression) is a function that runs immediately after being defined. It’s the foundation of the module pattern because it creates a new scope — a place where your variables and functions can’t be accessed from outside.

// Basic IIFE
(function() {
  const secret = "I'm hidden from the outside";
  console.log(secret); // Works inside
})();

// console.log(secret); // ReferenceError: secret is not defined

The key insight is that the IIFE creates a closure. The function inside “remembers” the environment where it was created, even after it finishes executing. This is what allows the returned object to access private variables.

The Revealing Module Pattern

A popular variation is the revealing module pattern, where you define all functions and variables privately, then explicitly expose references to the ones you want to be public. This makes it clear exactly what is part of the public API.

const UserManager = (function() {
  // Private state
  const users = [];
  const maxUsers = 100;
  
  // Private functions
  function validateUser(user) {
    return user && typeof user.name === "string" && user.name.length > 0;
  }
  
  function generateId() {
    return Math.random().toString(36).substring(2, 9);
  }
  
  // Public API - explicitly revealing what we want to expose
  return {
    addUser: function(user) {
      if (!validateUser(user)) {
        throw new Error("Invalid user object");
      }
      if (users.length >= maxUsers) {
        throw new Error("Maximum users reached");
      }
      const newUser = { ...user, id: generateId() };
      users.push(newUser);
      return newUser;
    },
    
    removeUser: function(id) {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) {
        return false;
      }
      users.splice(index, 1);
      return true;
    },
    
    getUser: function(id) {
      return users.find(u => u.id === id) || null;
    },
    
    getAllUsers: function() {
      return [...users]; // Return a copy to prevent mutation
    },
    
    getCount: function() {
      return users.length;
    }
  };
})();

// Usage
UserManager.addUser({ name: "Alice" });
UserManager.addUser({ name: "Bob" });
console.log(UserManager.getCount());    // 2
console.log(UserManager.getAllUsers());
// [{ name: "Alice", id: "abc123" }, { name: "Bob", id: "def456" }]

// These are private and can't be accessed:
// console.log(users);
// console.log(validateUser());

Namespacing with Modules

One of the main benefits of the module pattern is creating namespaces to avoid global pollution. Instead of adding dozens of functions to the global scope, you group related functionality under a single object.

// Instead of polluting the global scope:
function formatCurrency() { }
function formatDate() { }
function formatNumber() { }

// You create a namespace:
const Formatter = (function() {
  function currency(amount, locale = "en-US", currency = "USD") {
    return new Intl.NumberFormat(locale, {
      style: "currency",
      currency
    }).format(amount);
  }
  
  function date(dateObj, locale = "en-US") {
    return new Intl.DateTimeFormat(locale).format(dateObj);
  }
  
  function number(num, decimals = 2) {
    return num.toFixed(decimals);
  }
  
  return { currency, date, number };
})();

console.log(Formatter.currency(1234.56));    // $1,234.56
console.log(Formatter.date(new Date()));     // 3/17/2026
console.log(Formatter.number(42.567, 1));   // 42.6

Module Pattern vs ES6 Modules

Modern JavaScript uses ES6 modules (import/export), which offer similar benefits but with better tooling and syntax. Here’s how they compare:

// ES6 Module (modern.js)
const privateVariable = "hidden";

export function publicFunction() {
  return privateVariable;
}

export default { publicFunction };

// The module pattern (legacy-module.js)
const LegacyModule = (function() {
  const privateVariable = "hidden";
  
  function publicFunction() {
    return privateVariable;
  }
  
  return { publicFunction };
})();
FeatureModule PatternES6 Modules
SyntaxFunction-basedimport/export keywords
Static analysisLimitedFull (tree shaking)
Async loadingManualNative support
Circular dependenciesError-proneSupported
Private fieldsConvention (#)Truly private

When to Use Each

The module pattern is still useful in certain scenarios:

  1. Single-file encapsulation — When you need to isolate code in a single file without setting up a module bundler
  2. Legacy browser support — For environments that don’t support ES6 modules
  3. Dynamic module creation — When you need to create modules conditionally at runtime
  4. Learning JavaScript — Understanding the module pattern helps you understand how JavaScript scoping works

For new projects, prefer ES6 modules unless you have a specific reason not to.

Summary

The module pattern is a powerful tool for JavaScript developers:

  • Uses IIFEs and closures to create private state
  • The revealing module pattern makes public APIs explicit
  • Namespacing reduces global pollution
  • While ES6 modules are now preferred, the module pattern remains useful in specific scenarios

Understanding this pattern gives you insight into JavaScript’s scoping mechanics and helps you write more organized, encapsulated code.

See Also