How would you decide between mapping the domain class directly and translating to a separate persistence model at the boundary?
answer
- three positions, not two
- vocabulary out is not demands out
- duplication and translation are the price
- split where shapes actually diverge
- reads can bypass without splitting writes
basics
~20 sMap the domain class directly while its shape and the row's shape stay close and the mapper's demands are tolerable. Introduce a separate persistence model when the shapes genuinely diverge — accepting duplicated types and translation that must move together.
solid answer
~50 sThere are three positions, not two. **Declarations on the domain class** is the cheapest and most direct: one type, full query power, and mapping changes visible beside the members. **Declarations held outside the class**, in a document or startup code, keeps storage vocabulary out of the type while the mapper's structural demands still shape it — an empty construction path, open members, its own collections. **A separate persistence model** frees the domain type completely, at the price of a second set of types and translation on every read and write. Decide on evidence: how far the domain shape has drifted from the row shape, how sharply the demands bite, whether the schema is shared with systems you do not control, and whether the pressure is really coming from read paths. A partial split — translation only where it earns its keep — is usually better than a whole-codebase rule.
go deeper
Know that mapping declarations can sit on the domain class or that a separate set of storage types can exist with translation between them, and that the second means writing more code.
Be able to state what each arrangement costs: duplicated types and translation on one side, storage vocabulary and structural demands inside the domain type on the other.
Argue from evidence in a real codebase — how far the shapes have diverged, where invariants are already being defeated, and whether read projections would relieve the pressure without a split.
Own the reversibility question: decide per aggregate rather than codebase-wide, name the signals that would trigger a revisit, and account for the team's capacity to keep two models in step.
Where mapping metadata sits relative to the domain model is one of the few persistence decisions that is genuinely architectural: it is expensive to reverse, it shapes every type in the write path, and reasonable teams land in different places. The question is not whether storage should influence the domain, but how much influence to accept and where to pay to remove it. ## Three positions on one line 1. **Declarations on the domain class.** Storage vocabulary lives inside the type. One set of objects flows from the database to the business logic and back. 2. **Declarations outside the class.** The type carries no storage vocabulary, but the mapper's structural demands still apply to it: it must be constructible empty, its members must be reachable, and it must tolerate substitution. 3. **A separate persistence model.** Distinct types exist for storage, shaped to the schema, and translation happens at the boundary. The domain type is free of both the vocabulary and the demands. The second position is the one people forget. Moving declarations out of the class removes the visible coupling but not the structural one, so it buys less than it appears to. ## What a separate model buys - **The domain type can be shaped freely** — immutable, constructed only through operations that enforce invariants, composed of value types that have no natural column form. - **The schema can differ from the object graph** without contorting either: a row shape that suits querying and a domain shape that suits behaviour. - **Mapping churn stops touching domain code.** A column split or a table rename changes the persistence type and the translation, not the business rules. - **A schema shared with other systems** can evolve on its own schedule, with translation absorbing the difference. ## What it costs - **Two type sets that must change together.** Every new field is added twice and translated once, and any of the three can be forgotten. - **Translation code**, which is dull, voluminous and a real source of defects, especially around partially loaded graphs and collections. - **Weaker automation.** Change tracking still works, but it tracks the persistence type, so the layer no longer notices a domain change directly; something has to load, translate, mutate, translate back and write. - **Queries expressed against the persistence type**, which means the domain vocabulary does not appear in query code, or has to be reproduced there. ## Comparing the positions | | On the domain class | Outside the class | Separate model | |---|---|---|---| | Storage vocabulary in the domain type | Yes | No | No | | Structural demands on the domain type | Yes | Yes | No | | Types to maintain | One | One | Two plus translation | | Cost of a schema change | Touches domain code | Touches the mapping | Touches persistence type and translation | | Best when | Shapes are close | Shapes are close, vocabulary matters | Shapes genuinely diverge | ## How to decide 1. **Measure the divergence, do not assume it.** If the domain types today look like the rows, a separate model duplicates work to solve a problem you do not have. 2. **Ask how hard the demands bite.** A domain built on immutable values and constructor-enforced invariants collides with materialise-then-populate immediately. A domain of ordinary mutable aggregates barely notices. 3. **Look at the write path specifically.** Reads can often bypass the domain type altogether by projecting straight into a read shape, which relieves much of the pressure without splitting the write model. 4. **Consider schema ownership.** A schema you share with systems you do not control will evolve for reasons unrelated to your domain, and translation is the shock absorber. 5. **Consider who maintains it.** Two type sets and their translation need a team that will keep all three in step; a small team often gets more value from one set and a few concessions. ## Middle grounds worth naming The decision does not have to be uniform. A common and defensible arrangement maps most aggregates directly and translates only the two or three whose shapes have genuinely diverged. Another keeps one set of types but moves declarations outside them, accepting the structural demands while keeping the vocabulary out. A third serves reads through projections into purpose-built shapes while writes go through mapped types, which removes the largest source of pressure without doubling the write model. ## Signals that the current choice has stopped working Watch for: domain types acquiring members that exist only so a column has somewhere to live; invariants that cannot be enforced because the layer builds instances empty; schema changes that require touching business logic; or, in the other direction, translation code that is a mechanical field-for-field copy on every type, which means the split is paying nothing. Either signal is a reason to revisit, and revisiting one aggregate at a time is far cheaper than a codebase-wide reversal.
- Why does moving declarations out of the domain class buy less than it looks like?It removes storage vocabulary from the type but not the mapper's structural demands. The class must still be constructible without arguments, expose its members to the layer, tolerate a substituted subclass and give up ownership of its collections. The type looks clean and is still shaped by persistence.
- What is the strongest signal that a separate persistence model is not paying for itself?Translation code that is a mechanical field-for-field copy on every type. If the two models are structurally identical, the split is buying only indirection, and the cost — two definitions per change and three places to forget — is being paid for nothing. Collapse those types and keep the split only where shapes really differ.
- How can a team get most of the benefit without splitting the write model?Serve reads by projecting query results straight into purpose-built result shapes rather than loading mapped types and converting them. Most of the pressure on a mapped domain type comes from read paths that want a different shape; removing those leaves the write path, which is usually much closer to the row shape than the read path is.
- Should the decision be made once for the whole codebase?Rarely. The divergence between object shape and row shape is uneven across a system, so a uniform rule overpays in the parts that were already aligned and underpays in the parts that were not. Deciding per aggregate, with the same reasoning applied each time, keeps the cost proportional to the benefit.
saying these in an interview costs you the question
- Treats a separate persistence model as always the more professional choice
- Thinks moving declarations out of the class removes the mapper's structural demands
- Ignores the ongoing cost of keeping two type sets and translation in step
- Applies one rule to the whole codebase regardless of actual divergence
- Splits the write model when the pressure was coming from read paths