Online Compiler logoOnline Compiler
Advanced Patterns

Module Pattern: Creating Encapsulation with Closures

The Module Pattern leverages closures to create private and public interfaces, enabling data encapsulation and namespace management. It's a powerful design pattern that forms the foundation of modern modular JavaScript development.

What is the Module Pattern?

The Module Pattern uses closures to create private scope and expose a selective public interface. It combines Immediately Invoked Function Expressions (IIFE) with closures to create self-contained modules with controlled access to internal state and functionality.

Basic Module Pattern

const Counter = (function() {
  // Private variables
  let count = 0;
  
  // Private functions
  function log(msg) {
    console.log('[Counter] ' + msg);
  }
  
  // Public API (returned object)
  return {
    increment: function() {
      count++;
      log('Count is now: ' + count);
      return count;
    },
    
    get: function() {
      return count;
    },
    
    reset: function() {
      count = 0;
      log('Count reset');
    }
  };
})();

// Usage
Counter.increment(); // [Counter] Count is now: 1
Counter.increment(); // [Counter] Count is now: 2
console.log(Counter.get()); // 2
Counter.reset();  // [Counter] Count reset

// Private members are inaccessible
// console.log(Counter.count); // undefined

The module creates a private scope with hidden count variable. Only methods in the returned object can access it.

Core Concepts

IIFE (Immediately Invoked Function Expression)

IIFE Creates Module Scope

// IIFE: Function defined and called immediately
(function() {
  let privateVar = 'private';
  console.log(privateVar); // 'private' - only accessible here
})();

// console.log(privateVar); // ReferenceError

// Module returns public interface
const Module = (function() {
  let privateVar = 'private';
  
  return {
    getPrivate: function() {
      return privateVar; // Closure accesses privateVar
    }
  };
})();

console.log(Module.getPrivate()); // 'private'

IIFE creates a temporary scope. Returning an object creates closures that remember that scope.

Data Privacy and Encapsulation

Private Variables Cannot Be Modified Externally

const BankAccount = (function() {
  let balance = 1000; // Truly private, cannot be accessed directly
  
  return {
    deposit: function(amount) {
      if (amount > 0) {
        balance += amount;
        console.log('Deposited: ' + amount);
      }
    },
    
    withdraw: function(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        console.log('Withdrew: ' + amount);
      }
    },
    
    getBalance: function() {
      return balance;
    }
  };
})();

BankAccount.deposit(500);   // Deposited: 500
BankAccount.withdraw(200);  // Withdrew: 200
console.log(BankAccount.getBalance()); // 1300

// Cannot hack the account
// BankAccount.balance = 1000000; // Doesn't work
// BankAccount.balance is undefined

Private variables like balance cannot be accessed or modified from outside. Only the public methods can modify them.

Module Pattern Variations

Revealing Module Pattern

Define Functions, Reveal Only Public

const Calculator = (function() {
  // Private: Define all functions first
  function add(x, y) {
    return x + y;
  }
  
  function subtract(x, y) {
    return x - y;
  }
  
  function multiply(x, y) {
    return x * y;
  }
  
  function validateNumbers(x, y) {
    return typeof x === 'number' && typeof y === 'number';
  }
  
  // Public: Reveal only what's needed
  return {
    add: function(x, y) {
      if (!validateNumbers(x, y)) throw new Error('Invalid input');
      return add(x, y);
    },
    
    subtract: function(x, y) {
      if (!validateNumbers(x, y)) throw new Error('Invalid input');
      return subtract(x, y);
    },
    
    multiply: function(x, y) {
      if (!validateNumbers(x, y)) throw new Error('Invalid input');
      return multiply(x, y);
    }
  };
})();

console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
// Calculator.validateNumbers(5, 3); // TypeError

All functions are defined privately. The return statement selectively reveals only those needed in the public API.

Advanced Module Patterns

Singleton Pattern

Module Ensures Single Instance

const DatabaseConnection = (function() {
  let instance = null;
  
  function createConnection() {
    return {
      id: Math.random().toString(36).slice(2, 9),
      connected: true,
      query: function(sql) {
        console.log('Executing: ' + sql);
        return 'results';
      }
    };
  }
  
  return {
    getInstance: function() {
      if (!instance) {
        instance = createConnection();
        console.log('Connection created: ' + instance.id);
      }
      return instance;
    }
  };
})();

// Always returns same instance
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - Same object!

The module pattern naturally creates singletons. Each module is instantiated once and reused.

Module with Dependencies

Passing Dependencies as Parameters

// User module
const UserModule = (function() {
  let users = [];
  return {
    add: (name) => users.push(name),
    getAll: () => users.slice(),
    count: () => users.length
  };
})();

// Authentication using UserModule dependency
const AuthModule = (function(userService) {
  let authenticated = false;
  const validUsers = ['admin', 'user'];
  
  return {
    login: function(name) {
      if (validUsers.includes(name)) {
        authenticated = true;
        userService.add(name);
        console.log(name + ' logged in');
      }
    },
    
    isAuthenticated: function() {
      return authenticated;
    },
    
    getLoggedInCount: function() {
      return userService.count();
    }
  };
})(UserModule); // Pass dependency as parameter

// Usage
AuthModule.login('admin');
AuthModule.login('user');
console.log(AuthModule.getLoggedInCount()); // 2

Modules can accept dependencies as parameters. This enables better testing and loose coupling.

Augmentation Pattern

Extending Existing Modules

// Base module
const StringUtils = (function() {
  return {
    trim: (str) => str.trim(),
    toLowerCase: (str) => str.toLowerCase(),
    toUpperCase: (str) => str.toUpperCase()
  };
})();

// Augment the module without modifying original
(function(utils) {
  utils.capitalize = function(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };
  
  utils.reverse = function(str) {
    return str.split('').reverse().join('');
  };
  
  utils.isPalindrome = function(str) {
    const clean = str.toLowerCase().replace(/\s/g, '');
    return clean === utils.reverse(clean);
  };
})(StringUtils);

// Usage
console.log(StringUtils.capitalize('hello')); // 'Hello'
console.log(StringUtils.reverse('world')); // 'dlrow'
console.log(StringUtils.isPalindrome('racecar')); // true

Augmentation allows extending modules without modifying original code. Create new features externally.

Common Module Pattern Mistakes

❌ Exposing Private Variables

Mistake: Accidentally Exposing Internals

// WRONG - Exposes internals
const BadModule = (function() {
  let secretData = 'secret';
  let internalState = {};
  
  return {
    secret: secretData, // Exposes private variable!
    state: internalState, // Reference to internal object!
  };
})();

// Can modify internals from outside
BadModule.secret = 'hacked';
BadModule.state.pwned = true;

// CORRECT - Control what's exposed
const GoodModule = (function() {
  let secretData = 'secret';
  
  return {
    getSecret: function() {
      return secretData; // Read-only access
    },
    
    getState: function() {
      return { ...secretData }; // Return copy, not reference
    }
  };
})();

Only expose methods, not variables. Return copies or read-only access to prevent external modification.

❌ Memory Leaks from Closures

Mistake: Holding References Too Long

// PROBLEM - Closure holds large data even when unused
const Module = (function() {
  let largeArray = new Array(1000000).fill('data'); // Large object
  
  return {
    getValue: function() {
      return 42; // Doesn't use largeArray, but it's still held!
    },
    
    cleanup: function() {
      // Cleanup not provided - no way to free memory
    }
  };
})();

// SOLUTION - Provide cleanup and proper scoping
const ModuleFixed = (function() {
  let largeData = null;
  
  return {
    load: function() {
      largeData = new Array(1000000).fill('data');
    },
    
    getValue: function() {
      return 42;
    },
    
    unload: function() {
      largeData = null; // Free memory
    }
  };
})();

Closures capture their entire scope. Provide cleanup methods to free unused variables.

❌ Overcomplicating Simple Code

Mistake: Using Modules for Simple Functions

// OVERCOMPLICATION - Simple function doesn't need Module Pattern
const MathModule = (function() {
  return {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
})();

// BETTER - Keep it simple for simple cases
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// Use Module Pattern ONLY for:
// - Data privacy needs
// - Complex state management
// - Namespace organization
// - Preventing global pollution

Use Module Pattern when you need encapsulation. For simple utilities, keep it straightforward.

Module Pattern Best Practices

  • Use IIFE to create module scope immediately
  • Keep private variables and functions hidden - return only what's needed
  • Use Revealing Module Pattern for clarity - define all functions, reveal selectively
  • Accept dependencies as parameters for testability
  • Provide cleanup methods if module holds large resources
  • Use for complex state or when standard functions don't suffice

Module Pattern vs ES6 Modules

FeatureModule PatternES6 Modules
SyntaxIIFE (function)import/export keywords
EncapsulationClosuresLanguage level
Static AnalysisNot possibleFull support
Tree ShakingNot supportedFull support
Browser SupportAll versionsModern browsers

Related Topics

Frequently Asked Questions

When should I use the Module Pattern?

Use it when you need data privacy, complex state management, or organizing code into namespaces. For simple utilities, ES6 modules are often better.

What's the difference between Module Pattern and ES6 modules?

Module Pattern uses closures and IIFEs. ES6 modules use native language features with import/export, better tree-shaking, and static analysis support.

Can I modify private variables from outside a module?

No, truly private variables cannot be accessed externally. Only public methods can access and modify them. This is the power of the Module Pattern.

How do I test private functions?

Private functions are tested indirectly through the public API. If you need direct testing, consider exposing them or using a different architecture.

What about memory leaks with Module Pattern?

Modules hold their entire closure scope. If they reference large objects, that memory cannot be garbage collected. Provide cleanup methods when needed.

Can modules have multiple instances?

Yes. By default, modules are singletons. To create multiple instances, wrap the module in a factory function.