skip to content

In a web framework, what is a request transfer model, and why keep it separate from the domain object?

level: juniorimportance: must knowfreq 72%

answer

  1. two shapes, two rates of change
  2. the wire is a contract
  3. only the fields a caller may send
  4. declared fields are the write list
  5. no domain type in a handler signature

basics

~20 s

A request transfer model is a type that carries only the fields a client may send. Keeping it apart from the domain object lets each shape change at its own pace and stops clients from setting internal fields.

solid answer

~40 s

A transfer model is a boundary type: the framework deserializes the request body into it, checks declared constraints on it, and the handler then copies the values it wants onto the domain object. It exists because the wire shape and the domain shape answer different questions. The wire shape is a contract with clients and should change slowly and deliberately; the domain shape is internal and should be free to change whenever the model improves. Binding straight onto the domain object welds them together: every internal field becomes settable by a client, every rename becomes a breaking API change, and the set of fields a client may write is no longer written down anywhere. A transfer model makes that set explicit, because it literally is the list of fields the model declares.

go deeper

for a junior

Be able to say what a transfer model is in one sentence and name two reasons for it: the client cannot set internal fields, and the wire shape can change separately from the domain.

for a middle

Explain the mechanics: binding matches incoming names to declared properties, so the declared field set is the write set. Show why create and update endpoints usually need different transfer models.

for a senior

Show the production judgment: which surfaces genuinely need the separation, how you keep it from decaying, and how you enforce it mechanically rather than by reminding people in review.

for a principal

Frame the duplication as a cost you are deliberately buying protection with, and be able to say where you would not buy it — an internal surface with one client and a fast redeploy path is a legitimate exception.

A **transfer model** (often called a request DTO, an input model, or a command) is a type that exists only so the boundary has something of its own to bind into. The framework reads the request body, maps its fields onto this type, runs the declared constraints against it, and hands it to the handler. The handler then decides, in code it owns, which of those values reach the domain object. ## The two shapes answer different questions A **domain object** models the rules of the business: what a valid order is, which transitions an account may make, what an internal audit field records. A **transfer model** models a conversation: what this endpoint accepts from this caller right now. They look similar on the first day of a project, which is exactly why people collapse them — and then diverge, which is why the collapse hurts. | Concern | Wire shape (transfer model) | Domain shape | |---|---|---| | Audience | clients you may not control | code inside the service | | Rate of change | slow, negotiated, versioned | as fast as the model improves | | Field set | only what a caller may send | everything the invariant needs | | Breaking a name | breaks callers | a rename in one place | | Extra fields | must be rejected or ignored deliberately | irrelevant, nothing outside sets them | ## What the separation buys - **An explicit write set.** The declared fields of the transfer model *are* the list of things a client can change. Nobody has to reason about which domain fields happen to be settable. - **Independent evolution.** Adding an internal field to the domain object cannot accidentally appear on the wire or become settable; renaming one cannot break a caller. - **A place to put boundary concerns.** Formats, leniency, and declared constraints belong on a type owned by the boundary, not on the type that enforces business invariants. - **Different shapes per endpoint.** Creation, partial update, and administrative correction accept different field sets even though they all touch the same entity. One type cannot honestly describe three contracts. - **Honest validation.** Constraints on a transfer model describe what a *request* must look like; invariants on the domain object describe what the *business* requires. Mixed together, neither is trustworthy — a field that is optional on update but mandatory on creation has no single answer. - **A seam for testing.** A transfer model can be constructed in a test without building a valid aggregate, so boundary tests stay cheap. ## The cost, stated honestly The separation is duplication: a second type with similar fields, plus mapping code. That cost is real, and it is the reason teams argue about it. It is worth paying where the wire is a contract with someone you cannot redeploy, or where accidentally exposing or accepting a field would be a security event. It is easiest to skip on a small internal surface where the same team ships both sides — and that is a judgment call, not a law. ## How it goes wrong when the shapes are welded 1. A client sends a field the interface never renders. The binder recognises it as a property of the bound object and sets it. Nothing fails, because the field is legitimately part of that type. 2. Someone renames an internal field for clarity. Callers that send the old name silently stop having any effect, because a name that no longer matches simply does not bind. 3. Someone adds an internal flag — a review status, an internal price, a trust marker. It is now both readable and writable by anyone who can reach the endpoint, from the moment it is added. Each of those is the same root cause: the set of fields the outside world can touch was never written down, it was inherited from whatever the domain object happened to contain that week. ## Where the mapping lives Between the transfer model and the domain object sits mapping code. It can be written by hand, generated at build time, or performed by name-matching at runtime; the choice trades verbosity against how loudly a mismatch is reported. Whatever the mechanism, keeping it explicit is the point — it is the moment where someone decides that *this* incoming value is allowed to become *that* domain value, and that decision should be visible in the code rather than implied by two types happening to share a field name. A useful rule of thumb: **no domain type should appear in a handler's parameter list or in what a handler returns.** It is mechanical, it is easy to check in review or with an architecture test, and it preserves the boundary even in the parts of the codebase nobody is paying attention to.

  • If the domain object and the transfer model have identical fields today, is the separate type still worth it?
    Often yes, because the value is in what happens next year, not today: the wire shape can be frozen while the domain shape moves. But it is a judgment call. On a small internal surface where one team redeploys both sides, the duplication may cost more than it saves — the thing you must not lose is an explicit, reviewable list of the fields a client may write.
  • Should response models be separate types as well, or only request models?
    Both, but for different reasons and with different urgency. A request model controls what a client may change, so its absence is a write-side exposure. A response model controls what a client can see, so its absence leaks fields. Input models usually earn their keep first, because an accidental write is worse than an accidental read.
  • Where should validation constraints live if there is a transfer model and a domain object?
    Request-shaped rules go on the transfer model: required, length, format, range of accepted values. Business invariants stay in the domain object, where they hold regardless of which endpoint or job made the call. Duplicating a rule in both places is acceptable; moving an invariant out of the domain and into the boundary is not.

A shipping form is not the warehouse's inventory record. The form asks only what the customer is allowed to state; the clerk transcribes it into the record, deciding what actually changes.

saying these in an interview costs you the question

  • Transfer models are boilerplate; bind the entity and save time
  • The domain object already has the right fields, so just reuse it
  • Transfer models only matter for responses, not for request bodies
  • One type can serve create, update and admin edit equally well
  • Renaming an internal field is free even when it is bound from the wire