beginner By Mathias Paulenko

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.

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

The Module Pattern encapsulates private variables and functions inside a self-contained unit, exposing only a curated public API. It prevents namespace pollution, avoids global variable conflicts, and creates clear boundaries between unrelated parts of a codebase.

In JavaScript before ES6, this was achieved with Immediately Invoked Function Expressions (IIFE). Modern languages provide native modules (ES modules, Python packages, Java packages), but the pattern’s core idea — hiding internals and exposing interfaces — remains fundamental.

When to Use

Use the Module Pattern when:

  • You need private state that cannot be accessed from outside the module
  • Multiple components share a codebase and must avoid naming collisions
  • You want to expose a clean API while hiding implementation complexity
  • Testing requires clear boundaries between public contracts and private helpers

When to Avoid

  • The language provides native modules with proper encapsulation (use them instead)
  • Over-modularizing simple scripts adds unnecessary indirection
  • You need deep inter-module dependencies that create circular references

Solution

JavaScript (IIFE)

const CounterModule = (function () {
  let count = 0; // Private state

  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-private

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

Java

// com.myapp.counter.Counter.java
package com.myapp.counter;

public class Counter {
    private static int count = 0; // Package-private access

    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 Module Pattern relies on:

  • Private scope: Variables and functions exist only inside the module boundary
  • Public API: Explicitly exported functions form the contract with consumers
  • Single responsibility: Each module handles one concern (counting, formatting, HTTP)

Variants

VariantMechanismUse Case
IIFE ModuleClosure-based privacyPre-ES6 JavaScript
Revealing ModuleReturns an object literalCleaner API definition in JS
CommonJSmodule.exportsNode.js before ES modules
ES Moduleexport/importModern JavaScript standard
Namespace ModuleObject literal namespaceOrganizing utilities under one global

What Works

  • One concern per module. A module that counts, formats dates, and makes HTTP requests should be split.
  • Use explicit exports. Do not rely on wildcard exports; they hide the public contract.
  • Name private members clearly. Python uses _prefix; JavaScript closures hide them entirely.
  • Avoid circular dependencies. Module A importing B while B imports A causes runtime errors.
  • Keep modules small. A 500-line module is likely doing too much. Aim for 100-200 lines.

Common Mistakes

  • Global state in modules makes them untestable. A counter module with a global count cannot be reset between tests easily.
  • Wild-card exports (export *) leak internal helpers that become accidental public APIs.
  • Side effects on import like connecting to databases or registering event handlers make modules unpredictable.
  • Deeply nested module paths (a.b.c.d.e) create brittle import chains. Flatten where possible.
  • Treating modules as classes — modules are namespaces, not instantiable objects. Use classes or factories when you need multiple instances.

Real-World Examples

Node.js Core Modules

fs, path, and http are built-in modules that encapsulate OS operations behind clean APIs. None expose internal C++ bindings directly.

Python Standard Library

json, re, and urllib are modules that hide parsing engines and regex compilers behind simple function interfaces.

Angular Modules

NgModule decorators group components, services, and directives into cohesive feature units with explicit exports and imports.

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

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)

Lessons:

  • Module pattern: IIFE creates a closure with private state
  • Only what is returned in the object is public
  • apiKey, transactions and internal functions are private
  • In ES6+, use native import/export instead of IIFE
  • The module pattern is still useful for singletons with state
  • Namespacing: group related functionality under a module

### Module pattern vs ES modules: which do I use?

Use ES modules (import/export) for all new code: it is the standard, natively supported by browsers and Node.js. Use the module pattern (IIFE) only for legacy compatibility or when you need a singleton with private state without classes. ES modules are static: the bundler can tree-shake. The module pattern is dynamic: everything is included. For new libraries, ES modules. For inline scripts or legacy system compatibility, module pattern.

Frequently Asked Questions

Is the Module Pattern the same as a class?
No. A class is an instantiable blueprint. A module is a singleton namespace. Use modules for organization; classes for object creation.
Can modules have dependencies on each other?
Yes, but avoid circular dependencies. If A imports B and B imports A, refactor shared code into a third module C.
How do I test module-private functions?
Do not test them directly. If a private function is complex enough to need testing, extract it into its own module or make it package-private.