Design deep modules: a large amount of behavior behind a small interface, placed at a clean seam, testable through that interface. Everywhere you design and refactor code, use this vocabulary and these principles. The goal is: leverage for callers, locality for maintainers, testability for everyone.
Glossary
Use the following terms precisely — do not substitute "component", "service", "API", or "boundary". Using consistent language is exactly the point.
Module — Anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or cross-layer slice.Avoid using: unit, component, service.
Interface — Everything a caller needs to know to use the module correctly: type signatures, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics.Avoid using: API, signature (too narrow — they refer only to the type-level surface).
Implementation — What is inside the module, its code body. Different from Adapter : something can be a small adapter with a large implementation (such as a Postgres repository), or a large adapter with a small implementation (such as an in-memory fake). Use "adapter" when the discussion is focused on the seam; otherwise use "implementation".
Depth — Leverage at the interface: the amount of behavior a caller (or test) can invoke per unit of interface they must learn. When a module carries a large amount of behavior behind a small interface, it is deep; when the interface is almost as complex as the implementation, it is shallow。
Seam (Michael Feathers) — A place where behavior can be changed without modifying something elsewhere; the place where a module interface lives. Where a seam goes is itself a design decision, separate from what goes behind it.Avoid using: boundary (overloaded with DDD's bounded context).
Adapter — A concrete implementation that satisfies an interface at a seam. It describes therole(what slot it fills), not the substance (what is inside).
Leverage — The benefit callers get from depth: more capability per unit of interface they must learn. One implementation pays off across N call sites and M tests.
Locality — The benefit maintainers get from depth: changes, defects, knowledge, and verification are concentrated in one place rather than scattered across callers. Fix once, effective everywhere.
Deep vs shallow
Deep module = small interface + large implementation:
┌─────────────────────┐
│ 小型接口 │ ← 少量方法,简单参数
├─────────────────────┤
│ │
│ 深度实现 │ ← 隐藏的复杂逻辑
│ │
└─────────────────────┘
Shallow module = large interface + small implementation (avoid):
┌─────────────────────────────────┐
│ 大型接口 │ ← 大量方法,复杂参数
├─────────────────────────────────┤
│ 薄实现 │ ← 仅透传
└─────────────────────────────────┘
When designing an interface, ask yourself:
- 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 the implementation. A deep module can internally consist of small, mockable, replaceable parts — they just aren't part of the interface. A module can have internal seams(private to its implementation, used by its own tests) as well as external seams。
- The deletion test. Imagine deleting this module. If the complexity disappears, it is a pass-through. If the complexity reappears across N callers, then it is valuable.
- The interface is the test surface. Callers and tests cross the same seam. If you want to testacrossthe interface, the module's shape may have a problem.
- One adapter implies a hypothetical seam. Two adapters imply a real seam. Do not introduce a seam unless something actually varies at the seam.
Designing for testability
Good interfaces make testing natural:
-
Accept dependencies, don't create dependencies.
// 可测试 function processOrder(order, paymentGateway) {} // 难以测试 function processOrder(order) { const gateway = new StripeGateway(); } -
Return results, don't produce side effects.
// 可测试 function calculateDiscount(cart): Discount {} // 难以测试 function applyDiscount(cart): void { cart.total -= discount; } -
Small surface area. Fewer methods = fewer tests needed. Fewer parameters = simpler test setup.
Relationships
- Module has exactly one Interface(the surface it presents to callers and tests).
- Depth is a property of Module measured relative to its Interface .
- Seam is where the Module of Interface is located.
- Adapter is located at Seam , satisfying Interface。
- Depth produces Leveragefor callers and Locality。
Rejected framings
- Depth as the ratio of implementation lines to interface lines(Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- "Interface" as the TypeScript
interfacekeyword or a class's public methods: too narrow — the interface here includes every fact a caller must know. - "Boundary": overloaded with DDD's bounded context. Please say seam or interface。
Further reading
- Deepening a cluster given dependencies — see DEEPENING.md: dependency classification, seam discipline, and replacement rather than layered testing.
- Exploring alternative interfaces — see DESIGN-IT-TWICE.md: launching parallel sub-agents to design the interface in several very different ways, then comparing them in terms of depth, locality, and seam placement.