Book Studio

Modules Should Be Deep

Design modules whose simple interfaces hide substantial useful capability.

A module can be a class, a function, a service, or any boundary that separates an interface from an implementation. Its interface is everything a caller must know to use it correctly: names, parameters, return values, error rules, ordering constraints, and side effects. Its implementation is the machinery hidden behind that boundary.

A deep module provides significant capability through a small interface. A shallow module exposes nearly as much complexity as it contains. Depth is not measured by line count. It is the relationship between the benefit a module provides and the burden its interface places on every caller.

Compare the shapes

Module shapeInterfaceHidden capabilityEffect on callers
DeepSmall and predictableSubstantialCallers learn little and get much
ShallowWide or full of rulesModestCallers coordinate details themselves
LeakyLooks small but exposes hidden assumptionsUnreliableCallers fail when implementation details change

Suppose an application stores generated reports. A shallow API might ask every caller to choose a directory, construct a filename, select an encoding, write the bytes, and remember a retention tag. The storage class performs only the final file write. Its implementation is simple because the complexity has been pushed outward.

A deeper interface asks for the report and its business identity:

report-store.ts
type Report = {
  accountId: string;
  period: string;
  contents: string;
};

await reportStore.save(report);

The module can now own naming, encoding, storage location, atomic replacement, and retention metadata. The implementation is more complicated, but that complexity is paid once. Every caller gets the same policy without learning its details.

Prefer simple common cases

Interface design should optimize for the operations callers perform most often. Defaults can make the usual path short while explicit options preserve uncommon capabilities. The important point is not to minimize the number of methods at any cost. It is to minimize the knowledge required for correct use.

This is why many tiny classes do not automatically produce modular software. Splitting one coherent responsibility into a chain of wrappers can increase the number of names, objects, and handoffs without hiding any information. Each new boundary should absorb a meaningful decision or capability. Otherwise it merely creates another place a reader must visit.

Small is not the same as simple

A one-line wrapper can still add complexity if callers must understand both the wrapper and the object beneath it. A larger module can reduce complexity when its interface lets callers ignore the larger implementation.

Pull complexity downward

When a choice exists between making an implementation slightly harder and making every caller slightly harder, prefer the implementation. Validation, safe defaults, retries, and format decisions usually belong in the module that has the information to handle them consistently.

There is a limit: a module should not guess business policy it does not own. The report store can choose atomic file-writing mechanics, but it should not invent which accounts may generate reports. Depth comes from hiding implementation knowledge, not from collecting unrelated responsibilities.

Review a module by reading only its interface. Can a new caller complete the common task without studying the implementation? Are errors and side effects predictable? Does the interface speak in the caller's problem domain instead of the module's internal representation? If so, the module is likely doing useful work on behalf of the rest of the system.

On this page