--- name: codebase-design description: Use when designing or changing a module's interface, deciding where a seam goes, judging whether code is too shallow to be worth its surface, making something testable, or when another skill needs the deep-module vocabulary. --- # Codebase Design Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. The aim is leverage for callers, locality for maintainers, and testability for everyone. This is the reference for that language; it is consulted, not run. ## Glossary Use these terms exactly. Do not substitute "component", "service", "API", or "boundary": the consistent language is the whole point, because a shared word is what lets a design discussion stay about the design. **Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, a class, a package, or a 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, which are too narrow because they name only the type-level surface. **Implementation**: what is inside a module, its body of code. Distinct from **adapter**: a thing can be a small adapter with a large implementation (a Postgres repository) 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 a test can exercise per unit of interface it has 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, separate from what goes behind it. *Avoid*: boundary, which is overloaded with DDD's bounded context. **Adapter**: a concrete thing that satisfies an interface at a seam. Names a *role*, the slot it fills, not its substance. **Leverage**: what callers get from depth. More capability per unit of interface learned. 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 instead of spreading across callers. Fix once, fixed everywhere. ## Deep vs shallow A **deep module** is a small interface over a large implementation: ``` ┌─────────────────────┐ │ Small interface │ ← few entry points, simple params ├─────────────────────┤ │ │ │ Deep implementation │ ← complex logic hidden │ │ └─────────────────────┘ ``` A **shallow module** is a large interface over a thin implementation, and is the thing to avoid: ``` ┌─────────────────────────────────┐ │ Large interface │ ← many entry points, complex params ├─────────────────────────────────┤ │ Thin implementation │ ← mostly passes through └─────────────────────────────────┘ ``` When designing an interface, ask: can I reduce the number of entry points, simplify the parameters, or hide more complexity inside? ## Principles - **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, swappable parts; they simply are not part of the interface. A module can have **internal seams**, private to its implementation and 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 interface is the test surface.** Callers and tests cross the same seam. Wanting to test *past* the interface means the module is probably the wrong shape. - **One adapter means a hypothetical seam. Two adapters means a real one.** Introduce a seam only when something actually varies across it. ## Designing for testability Good interfaces make testing natural. **Accept dependencies, do not create them:** ```typescript // Testable: the seam is a parameter function processOrder(order: Order, gateway: PaymentGateway) {} // Hard to test: the dependency is welded in function processOrder(order: Order) { const gateway = new StripeGateway(); } ``` **Return results, do not produce side effects:** ```typescript // Testable: the outcome is the return value function calculateDiscount(cart: Cart): Discount {} // Hard to test: the outcome is a mutation function applyDiscount(cart: Cart): void { cart.total -= discount; } ``` **Keep the surface small.** Fewer entry points mean fewer tests; fewer params mean simpler setup. ## Relationships - 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**. - A **seam** is where a **module**'s **interface** lives. - An **adapter** sits at a **seam** and satisfies the **interface**. - **Depth** produces **leverage** for callers and **locality** for maintainers. ## Rejected framings - **Depth as the ratio of implementation lines to interface lines** (Ousterhout): rewards padding the implementation. Depth-as-leverage is the definition used here. - **"Interface" as the TypeScript `interface` keyword, or a class's public methods**: too narrow. The interface includes every fact a caller must know, including the ones the types cannot carry. - **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. ## Going deeper - **Deepening a cluster given its dependencies**: [DEEPENING.md](DEEPENING.md) covers the dependency categories, seam discipline, and replace-don't-layer testing. - **Exploring several interfaces for one module**: [DESIGN-IT-TWICE.md](DESIGN-IT-TWICE.md) covers the parallel design pattern, and the gate for when it is worth the ceremony.