Across a large service, when does duplicating domain shapes into per-endpoint transfer models stop paying for itself, and how do you govern it?
answer
- who consumes it, and how fast can you redeploy them
- input and output are not symmetric
- privileged fields dominate the decision
- default per surface, not per developer
- enforce the write boundary in the build
basics
~20 sDuplication buys a stable wire contract and an explicit write set; it stops paying when one consumer redeploys with the server and no field is privileged. Govern it per surface, enforcing the write-side boundary mechanically rather than per developer.
solid answer
~50 sTransfer models trade ceremony for two properties: the wire shape can change independently of the domain, and the set of fields a client may write is written down. Those are worth most where consumers are numerous or outside your release cycle, where a shape is long-lived, and where an accidentally exposed or accepted field would be a security event. They are worth least on an internal surface with one consumer that ships in the same deployment. The governing mistake is leaving the decision to each developer, which produces a codebase where the answer to "what can a client change here?" varies by endpoint. Pick the default consciously per surface, enforce the write-side boundary mechanically — a rule that no domain type reaches a handler signature — and allow the relaxation only on the read side of clearly-marked internal surfaces.
go deeper
Know that the separation has a real cost in extra types and mapping, and that the usual reason to pay it is protecting what a client can change.
Name the axes: number of consumers, how fast you can redeploy them, whether privileged fields exist, and how often the domain model moves.
Show the middle grounds — per-resource models, generating one direction, generating from a contract description — and where you would still refuse to relax on the write path.
Own the governance: a default set per surface, an automated boundary rule on writes, located exemptions, and a named trigger that forces re-examination when the audience changes.
Nobody argues that a boundary type is free. The argument is about where the price is worth paying, and it is a judgment call with no single right answer — which is exactly why it should be decided once per surface rather than re-litigated per endpoint. ## What the duplication actually buys - **Independent evolution.** The wire shape can be frozen while the domain model is refactored, split, or renamed. Without it, every internal improvement is an API negotiation. - **An explicit write set.** The declared fields answer "what can a client change?" in one place, permanently, for anyone reading the code. - **Per-endpoint honesty.** Create, partial update, and administrative correction really do accept different fields; one type cannot state three contracts. - **A blast-radius ceiling.** A field added to the domain object cannot become writable or visible by accident. - **Cheap boundary tests.** You can build a request object without constructing a valid aggregate. ## What it costs - More types, and mapping code for each. - A second place to change when a field is genuinely added end to end. - Drift risk of its own: a transfer model nobody updates quietly stops exposing something it should. - Real friction on small surfaces, where the ceremony is a visible fraction of the endpoint. ## The axes that decide it | Axis | Duplication pays | Duplication is hard to justify | |---|---|---| | Consumers | many, or outside your release cycle | one, deployed with the server | | Shape lifetime | long-lived, negotiated | changes freely with the code | | Privileged fields | present on the domain type | none; every field is client-settable anyway | | Blast radius of a mistake | a security or compliance event | a bug someone reports the same day | | Domain volatility | model refactored often | model is stable and boring | Two of those deserve emphasis. First, **write and read are not symmetric**. An input model decides what a client may *change*; an output model decides what it may *see*. Both matter, but an accidental write is usually worse than an accidental read, so where you spend a limited appetite for ceremony, spend it on input first. Second, **presence of privileged fields dominates the table**. If the domain type holds an ownership, role, money or state field the server controls, the separation is not a style preference; it is the mechanism that keeps those fields unreachable. ## Middle grounds worth naming 1. **Per-resource rather than per-endpoint models.** One well-designed input model per resource plus explicit constraints per operation, instead of a type per handler. Less ceremony, most of the protection; the cost is that a field needed by only one endpoint becomes bindable on all of them, which you accept knowingly or not at all. 2. **Derive one direction, hand-write the other.** Generate or project response models from domain types while writing input models by hand. This spends the effort where the asymmetry above says it belongs. 3. **Generate models from an API description.** When a shape description is already the contract, models generated from it are duplication a machine maintains, and the wire shape becomes the artifact under review. ## How to govern it - **Set the default per surface, not per developer.** "Public and partner surfaces always use transfer models; this internal administrative surface may bind read models directly" is a decision that can be reviewed. Leaving it to taste produces a codebase where the answer varies endpoint by endpoint and nobody can audit it. - **Enforce the write side mechanically.** A build-time architecture rule that no domain type appears in a handler's parameters removes the failure class from the endpoints nobody reviews. Silent classes of bug need automated guards. - **Make exceptions explicit and located.** An exemption list that a reviewer can read beats an unwritten norm, and it gives you the inventory when the surface later goes public. - **Review the contract, not the diff.** If the wire shape is the thing with consumers, the artifact under review should be the shape, not the incidental change that altered it. - **Revisit when the audience changes.** The single strongest trigger is a surface gaining a consumer you cannot redeploy. That is the moment the cheap option stops being cheap, and the migration is far more expensive after the consumers exist than before. ## The honest summary Duplication is not a virtue and neither is brevity. What you are really buying is the ability to answer two questions at any moment — *what can a client change here?* and *can I rename this field?* — without reading the whole service. Any arrangement that keeps both answers cheap is defensible; any arrangement where the answer is "whatever the domain type currently declares" is one refactor away from an incident.
- What single trigger should force a surface that skipped transfer models to adopt them?Gaining a consumer you cannot redeploy on your own schedule. Until then, a contract change is a same-change edit on both sides; afterwards, every internal rename becomes a negotiation. Migrating before the consumers exist is cheap, and after they exist it is a versioning exercise, so treat the first external consumer as the deadline rather than the prompt.
- If you could enforce only one rule mechanically, which would you pick?That no domain type appears in a handler's parameter list. It is checkable in the build, it targets the write path where accidental exposure is worst, and it fails on the endpoints nobody is reviewing. Output-side rules matter too, but a leaked field is usually recoverable in a way an unauthorized write is not.
- How do you answer a team that says the mapping ceremony is slowing them down?Take the complaint seriously and change the shape of the cost rather than removing the control: move to per-resource models, generate one direction, or generate both from the contract description. Keep the property that the client-writable field set is explicit. If the surface genuinely has one redeployable consumer and no privileged fields, granting the exemption on that surface is a legitimate answer.
saying these in an interview costs you the question
- Transfer models are always correct regardless of the surface
- Skip them everywhere; mapping is pure ceremony
- Input and output models carry identical risk
- Each developer should decide per endpoint what feels right
- An internal surface never becomes an external one