Home Design Patterns Module

Module

Modern Practice

Group related things behind one deliberately narrow public surface.

Purpose

A Module is a unit of code with two properties: the things inside it belong together, and most of them cannot be named from outside. The first half is cohesion and it is a matter of taste. The second half is the pattern, and it is enforceable. A module is defined by what it refuses to export.

This is the Facade instinct applied to a whole unit of code rather than to a handful of collaborating objects. A Facade offers a convenient door and leaves the rest of the subsystem reachable. A Module makes the rest unreachable, so the narrow surface is not a convenience, it is the contract. That difference matters, because what you can change safely tomorrow is exactly the set of things nobody could name today.

Design

In practice you do not build this out of classes. You use whatever construct the language provides, because every mainstream language now has one: a .NET assembly with internal, a Java package or a Java 9 module with an exports clause, an ES module where anything without export is simply unreachable, a Python module with __all__ and the leading-underscore convention, a C++20 export module. The pattern is a description of what those constructs are for, not a recipe for reimplementing them.

UML class diagram: a Reporting client calling a Vision module, which holds three internal classes that the client cannot name.
The arrows out of Vision all point at things Reporting cannot mention. The public surface is two operations wide; the module behind it is three classes deep and free to become five.

The design work is choosing the boundary, and the useful rule is Parnas's from 1972: draw the line around a decision that is likely to change, not around a step in the process. A module named after a stage of the pipeline splits along the flow of data, so a change to a decision cuts across every module at once. A module named after the decision absorbs the change entirely.

Example

Autobot Robotics's vision system started as a set of classes in a shared project: a frame grabber, a blob finder, a lens distortion model, and a coordinate transform. Everything was public, because nothing had a reason not to be.

Then the office-side reporting tool wanted a picture of the part next to each inspection result, and the quickest way to get one was to call FrameGrabber.grab() directly. Some months later the vision team changed the grabber so that a returned frame stayed valid only until the next capture, which was correct for the vision pipeline and fine for every caller they knew about. Reporting was not a caller they knew about. It started writing torn images into customer reports, and nobody connected the two changes for a fortnight because there was no reason to look at reporting when the vision code changed.

The team marked the grabber, the blob finder and the lens model internal, and published locate, calibrate and a new snapshot that returned a frame the caller owned. Reporting's genuine need became one documented entry point instead of an accident. The important result is not that the bug was fixed; it is that the vision team can now change buffer ownership again and the compiler will tell them who is affected.

namespace Autobot.Sensing
{
    // The entire public surface: one class, three operations.
    public static class Vision
    {
        public static Pose locate(Part p) { /* some code */ }
        public static void calibrate()    { /* some code */ }
        public static Frame snapshot()    { /* some code */ }
    }

    // internal: nameable inside this assembly, invisible outside it.
    internal class FrameGrabber
    {
        public Frame grab() { /* some code */ }
    }

    internal class BlobFinder { /* some code */ }
    internal class LensModel  { /* some code */ }
}

// A different assembly in the same solution.
namespace Autobot.Reporting
{
    class InspectionReport
    {
        public void build()
        {
            Frame f = Autobot.Sensing.Vision.snapshot();

            // FrameGrabber g = new FrameGrabber();
            //   does not compile: FrameGrabber is inaccessible
            //   due to its protection level
        }
    }
}
The pattern is the word internal, repeated three times, and the fact that the public surface is three operations rather than four classes. Nothing else here is doing any work.

Seen in the Wild

Java 9 added a real module system: module-info.java declares requires and exports, and a package that is not exported cannot be read by another module even by reflection, which is what finally made sun.misc.Unsafe hard to reach. .NET has assemblies plus the internal modifier, with InternalsVisibleTo as the deliberate escape hatch for test projects. ES modules make it structural: a binding without export is not merely discouraged, it does not exist outside the file. Python is the weakest of the four, relying on __all__ and a leading underscore that nothing enforces. The idea itself is older than all of them: Parnas's 1972 paper "On the Criteria To Be Used in Decomposing Systems into Modules" is still the clearest statement of why the boundary should follow a design decision rather than a processing step.

Comparisons

  • Facade is the same instinct at object scale. A Facade simplifies access and leaves the subsystem reachable; a Module makes the inside unreachable, so it can enforce what a Facade can only suggest.
  • Extensibility is what you get when the modules are designed to be added to rather than only to be hidden behind.
  • Aggregate draws the same kind of boundary around data rather than around code, with one entry point and one consistency rule.
  • Singleton is what a module turns into when you give it mutable state. A module of pure functions is fine to reach globally; a module holding a connection is a global variable with a namespace on it.
  • Dependency Injection is how a module gets its collaborators without naming them, which is what keeps the dependency graph between modules acyclic.

Why It Exists

My take

Every symbol that can be reached is a symbol that will be reached. That is the entire pressure behind this pattern and it is not a statement about discipline, it is a statement about time. In a codebase that lives ten years, the question "who calls this function" has an answer you cannot know: it is whoever found it in autocomplete on a Tuesday in a year you have forgotten. The vision bug in the example above is the normal case, not an unusual one. Nobody did anything wrong; the grabber was public, so it was part of the contract, and nobody had ever decided that.

I learned this the hard way in C, where the language gives you nothing. The token-ring stack I wrote had modules, but they were an act of will: a public header with the declarations you were meant to use, a private header for the rest, static on every file-scope function that was not exported, and a naming prefix on everything that was, because there were no namespaces and two modules with a function called reset() is a link error. That was the whole enforcement mechanism, and it was weak. Anything not marked static was reachable from anywhere in the image by simply declaring it yourself, and people did, because the alternative was waiting for someone to add an accessor. The linker was the only thing standing between a layered design and a plate of spaghetti, and the linker did not care.

So my honest position is that Module is the pattern you should almost never implement, because you should be using the one your language already has. If you find yourself writing a class called SomethingModule whose job is to gather up other classes, stop: you have reinvented Facade, or Singleton, and you are getting none of the enforcement that made the pattern worth naming. The useful skill here is not construction, it is knowing where to draw the line and then having the nerve to keep things behind it when someone asks nicely.

Criticisms

Module boundaries get drawn in the wrong place more often than not, and the wrong place has a recognizable smell: a module named after a layer or a noun rather than a decision. utils, helpers, common and core are modules with no design decision inside them, so nothing about them can be changed without affecting everybody, and because everything depends on them they sit at the bottom of the dependency graph where nothing can be removed. Cohesion is not "these things are all small".

Narrow surfaces also only ever widen. Every export exists because somebody had a real need, and no one has ever been rewarded for deleting one. The first version publishes three operations, then a legitimate case appears and it is four, and the count only goes in one direction because removing an export breaks a caller you cannot see. Java's sun.misc.Unsafe is the extreme version: never intended as public API, depended on by so much of the ecosystem that removing it took a decade and a whole new module system.

Finally, the enforcement is usually softer than it looks. internal plus InternalsVisibleTo is a boundary with a documented hole in it, Python's underscore is pure convention, and reflection defeats most of it in most languages. And there is a granularity trap at the other end: split a system into sixty modules and the dependency graph between them becomes the thing you cannot understand, which is the problem you were trying to solve.