# Model Layering The question of whether API, domain, and storage layers should each have their own types, or whether types should be shared across layers. ## The core tension Sharing types across layers reduces boilerplate and translation code. Separate types per layer give precise control over what each layer exposes and accepts, but multiply the number of types and require translation code between them. ## Key concerns **The escape problem.** API types leak into domain and storage layers over time. Once a function takes an `api.Bots_CreateReq`, that dependency is hard to reverse. The only reliable enforcement is package structure — the build breaks if domain or storage packages import API packages. Convention alone doesn't hold. If API types embed domain types directly, the import cycle provides this enforcement for free: `demo/api` imports `demo/lib/bots`, so `demo/lib/bots` can't import `demo/api`. If API types are kept separate (no domain type embedding), the cycle disappears and a linter rule is the right replacement — e.g. `depguard`: "store/domain packages may not import the api package." An `internal` package doesn't help here: it would block domain packages from importing API types, but the organizational result is awkward (types buried in internal, impl at the root) and it says nothing about what direction the API types themselves import. **Computed fields.** Some fields are set by the system, not the caller: IDs (if server-assigned), timestamps, derived state. A single shared type can't cleanly express "this field is writable on create," "this field is writable on update," and "this field is read-only." The same problem exists at the storage layer (e.g. `created_at`). Several approaches were considered and rejected: - *Spec/status split* (`BotSpec` for writable fields, `Bot` for the full entity) — clean in simple cases but gets awkward when create and update diverge naturally, and introduces types that exist only to serve the split rather than representing meaningful domain concepts. - *Explicit per-operation API types* (no domain type embedding, each request/response spells out its fields) — eliminates the problem at the API layer but doesn't solve it at domain or storage layers, adds translation boilerplate, and still relies on convention to prevent embedding computed-field types in requests. - *Output-only tags* — a framework mechanism to mark computed fields and strip/reject them on input. Maps cleanly to OpenAPI `readOnly` and could apply consistently across all layers. Rejected because it adds framework complexity that every engineer must learn. The computed fields problem has no clean solution that doesn't introduce significant complexity. The practical answer is convention and review: handlers ignore or reject unexpected values for computed fields, and engineers learn which fields those are. **Validation.** Invariants should live in the domain layer so any caller — API handler, provisioner, background job — gets them enforced. The API layer adds user-facing error formatting and input coercion, not business rules. Validation logic in API types that must be invoked manually by non-API callers is a footgun. **Multiple API versions.** Separate API types make versioning essentially free: v1 and v2 types can both map to the same domain type. If API types are shared with domain logic, versioning becomes very painful. However, versioning is not a cost worth paying upfront — many systems never need it, and there are many ways to approach it. It can be layered on when needed. **Naming decay.** Naming inconsistency grows when layers are added reactively. A framework that defines the layer conventions upfront gives downstream projects a template to follow. ## Current direction Reuse domain types in the API layer. The escape problem is handled by the import cycle as a side effect. The computed fields problem exists at all layers and has no clean mechanical solution; convention is the answer. Translation boilerplate is eliminated entirely. This may be revisited if a clean solution to computed fields emerges, or if versioning becomes a real requirement. ## Open questions - Where does the framework draw the line on how many layers are "right"? API → domain → storage is three; some projects add a service/use-case layer between domain and API.