Design deep modules: put a lot of behavior behind a small interface, place the interface on a clear seam, and test through that interface. Whenever you are designing or refactoring code, use this language and these principles. The goal is to bring leverage to callers, locality to maintainers, and make testing easier for everyone.
Glossary
Use these terms precisely; do not substitute "component", "service", "API", or "boundary". Consistent language is the point.
Module - Anything that has an interface and an implementation. It is deliberately unconstrained in scale: a function, class, package, or cross-layer slice all count.Avoid: unit, component, service.
Interface - Everything a caller must know to use a module correctly: the type signature, plus invariants, ordering constraints, error modes, required configuration, and performance characteristics.Avoid: API, signature (too narrow; refers only to the type-level surface).
Implementation - The body of code inside a module. It is different from Adapter: something can be a small adapter but have a large implementation (Postgres repo), or be a large adapter but have a very small implementation (in-memory fake). Say adapter when discussing a seam; otherwise say implementation.
Depth - Leverage on an interface: how much behavior a caller (or test) can reach for each unit of interface learned. When a lot of behavior hides behind a small interface, the module is deep; when the interface is almost as complex as the implementation, the module is shallow。
Seam(Michael Feathers)- A place where you can change behavior without editing the current location; that is, the locationwhere a module's interface lives. Where the seam goes is an independent design decision, separate from what goes behind the seam.Avoid: boundary (it overloads with DDD bounded context).
Adapter - A concrete thing that satisfies some interface at a seam. It describes the role(which slot it fills), not the substance (what is inside).
Leverage - The benefit callers get from depth: for each unit of interface learned, they get more capability. An implementation pays for itself across N call sites and M tests.
Locality - The benefit maintainers get from depth: change, bugs, knowledge, and verification are concentrated in one place instead of scattered across callers. Fix once, fixed everywhere.
Deep and shallow
Deep module = small interface + lots of implementation:
+------------------+
| Small Interface | -> few methods, simple params
+------------------+
| |
| Deep | -> complex logic hidden
| Implementation |
| |
+------------------+
Shallow module = large interface + little implementation (avoid):
+-------------------------------+
| Large Interface | -> many methods, complex params
+-------------------------------+
| Thin Implementation | -> mostly pass-through
+-------------------------------+
When designing an interface, ask:
- Can I reduce the number of methods?
- Can I simplify the parameters?
- Can I hide more complexity internally?
Principles
- Depth is a property of the interface, not of the implementation. A deep module can internally consist of small, mockable, swappable parts; they just do not belong to the interface. A module can have both internal seams(implementation-private, used by its own tests) and at the interface's external seam。
- Deletion test。 Imagine deleting this module. If the complexity disappears, it was just a pass-through. If the complexity scatters back across N callers, it is providing value.
- Interface is the test surface。 Callers and tests cross the same seam. If you want to test internal details behind the interface, this module shape may be wrong.
- One adapter means a hypothetical seam. Two adapters means a real one. Do not introduce a seam unless something actually varies across the seam.
Design for Testability
Good interfaces let tests happen naturally:
-
Accept dependencies, don't create them.
// Testable function processOrder(order, paymentGateway) {} // Hard to test function processOrder(order) { const gateway = new StripeGateway(); } -
Return results, don't produce side effects.
// Testable function calculateDiscount(cart): Discount {} // Hard to test function applyDiscount(cart): void { cart.total -= discount; } -
Small surface area. Fewer methods = fewer tests needed. Fewer params = simpler test setup.
Relationships
- A Module has exactly one Interface(the surface it presents to callers and tests).
- Depth is a property of Module and is measured by its Interface .
- Seam is where the Module of Interface is located.
- Adapter sits on Seam and satisfies Interface。
- Depth produces Leveragefor callers and Locality。
Formulations not to adopt
- Treating depth as the ratio of implementation-lines to interface-lines(Ousterhout): this rewards padding the implementation. Here we use depth-as-leverage.
- Understanding "Interface" as the TypeScript
interfacekeyword or class public methods: too narrow; the interface here includes all facts the caller must know. - "Boundary": overloaded with DDD bounded context. Say seam or interface。
Further deepening
- Deepening a cluster given its dependencies - see DEEPENING.md: Dependency classification, Seam discipline, and the "replace, don't stack" testing strategy.
- Exploring alternative interfaces - see DESIGN-IT-TWICE.md: Launch parallel sub-agents to design the interface in several very different ways, then compare by depth, locality, and seam placement.