99 unchanged lines
Use these terms exactly: don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
Use these terms exactly: don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
**Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.
**Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.
**Interface**: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow, they refer only to the type-level surface).
**Interface**: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow, they refer only to the type-level surface).
**Implementation**: what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
**Implementation**: what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
**Depth**: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.
**Depth**: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.
**Seam** _(Michael Feathers)_: a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).
**Seam** _(Michael Feathers)_: a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).
**Adapter**: a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).
**Adapter**: a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).
**Leverage**: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests.
**Leverage**: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests.
**Locality**: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.
**Locality**: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.
## Deep vs shallow
## Deep vs shallow
**Deep module** = small interface + lots of implementation:
**Deep module** = small interface + lots of implementation:
┌─────────────────────┐
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
├─────────────────────┤
│ Deep Implementation│ ← Complex logic hidden
│ Deep Implementation│ ← Complex logic hidden
└─────────────────────┘
└─────────────────────┘
**Shallow module** = large interface + little implementation (avoid):
**Shallow module** = large interface + little implementation (avoid):
┌─────────────────────────────────┐
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
└─────────────────────────────────┘
When designing an interface, ask:
When designing an interface, ask:
- Can I reduce the number of methods?
- Can I reduce the number of methods?
- Can I simplify the parameters?
- Can I simplify the parameters?
- Can I hide more complexity inside?
- Can I hide more complexity inside?
## Principles
## Principles
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable 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 the **external seam** at its interface.
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable 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 the **external seam** at its interface.
- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
## Designing for testability
## Designing for testability
Good interfaces make testing natural:
Good interfaces make testing natural:
1. **Accept dependencies, don't create them.**
1. **Accept dependencies, don't create them.**
```typescript
```typescript
function processOrder(order, paymentGateway) {}
function processOrder(order, paymentGateway) {}
// Hard to test
// Hard to test
function processOrder(order) {
function processOrder(order) {
const gateway = new StripeGateway();
const gateway = new StripeGateway();
2. **Return results, don't produce side effects.**
2. **Return results, don't produce side effects.**
```typescript
```typescript
function calculateDiscount(cart): Discount {}
function calculateDiscount(cart): Discount {}
// Hard to test
// Hard to test
function applyDiscount(cart): void {
function applyDiscount(cart): void {
cart.total -= discount;
cart.total -= discount;
3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.
3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.
## Relationships
## Relationships
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
- **Depth** is a property of a **Module**, measured against its **Interface**.
- **Depth** is a property of a **Module**, measured against its **Interface**.
- A **Seam** is where a **Module**'s **Interface** lives.
- A **Seam** is where a **Module**'s **Interface** lives.
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
## Rejected framings
## Rejected framings
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow: interface here includes every fact a caller must know.
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow: interface here includes every fact a caller must know.
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.