Modules, interfaces and APIs
Hide detail behind a small, stable surface so each part can change without breaking the rest.
What you will learn
- 01Why we draw boundaries
- 02Interface and implementation
- 03Designing a good interface
- 04APIs between programs
- 05Cohesion and coupling
- 06A checklist
Why we draw boundaries
No one can hold a whole program in their head. A module is a group of related data and functions with a boundary around it, so that someone using it needs to know only what it does, not how. The guiding idea, from the early days of software design, is to hide the decisions most likely to change. If a decision is hidden, changing it costs one module, not the whole program.
Swipe sideways to pan · drag a box to move it
Interface and implementation
Every module has two faces. The interface is what other code may use: names, inputs, outputs and errors. The implementation is everything behind it. Callers depend on the interface only.
// lot.ts
const tickets = new Map<string, Ticket>(); // private: not exported
export function enter(plate: string, now: number): Ticket {
const t = { id: crypto.randomUUID(), plate, enteredAt: now };
tickets.set(t.id, t);
return t;
}
export function leave(id: string, now: number): number {
const t = tickets.get(id);
if (!t) throw new Error("unknown ticket");
tickets.delete(id);
return fee((now - t.enteredAt) / 60000);
}
The map is not exported, so no outside code can corrupt it. Tomorrow you can replace the map with a database and no caller changes, because callers never knew it existed.
Swipe sideways to pan · drag a box to move it
Designing a good interface
A good interface is small, honest and hard to misuse.
- Small: expose the fewest operations that let callers do their job. Every extra function is a promise you must keep.
- In the caller's language:
enterandleave, notinsertRowanddeleteRow. - Hard to misuse: use types so wrong calls fail early. A
Ticketcannot be confused with a plate string. - Complete about failure: say what happens when the ticket is unknown. Surprising errors are part of the interface too.
- Never leak internals: do not return the internal map. Return values, or copies.
Swipe sideways to pan · drag a box to move it
APIs between programs
The same ideas apply when the boundary is a network. An API is an interface offered to other programs, and an HTTP endpoint is just a function you call remotely. A contract states the inputs, outputs and errors, and callers are allowed to rely on it.
The cost of change is higher because you may not control the callers. Two habits help. Adding something optional is safe, while removing or renaming something breaks people. And when a break is unavoidable, introduce a new version and keep the old one until callers have moved.
Cohesion and coupling
Two words describe how well boundaries are drawn.
- Cohesion is how closely the contents of a module belong together. High cohesion means everything in it changes for the same reason.
- Coupling is how much one module depends on the details of another. Low coupling means few, simple dependencies.
Aim for high cohesion and low coupling. When one small change forces edits in many modules, coupling is too high. Avoid circular dependencies, where two modules need each other, because neither can be understood or tested alone.
A checklist
- Name what each module hides.
- Export only what callers need.
- Keep state private and expose operations.
- Write down the contract, including errors.
- Prefer depending on an interface over depending on a concrete detail.