skip to content

Value Types & Converters

The conversion boundary: values flattened into the owner's columns, and converters for enumerations, money and wrapped identifiers. Probed because filtering on a converted value often misses rows.

on this pageshow

questions

6

In a data-access layer, what does it mean to flatten a multi-field value object into its owner's columns?

level: juniorimportance: must knowfreq 62%

answer

  1. part of the owner's row
  2. no key, no independent lifecycle
  3. column names come from the mapping
  4. two owners of one type collide

basics

~20 s

Flattening stores a composite value's fields as extra columns on the owner's own row rather than in a table of its own. The value gets no key and no independent lifecycle: it is loaded, written and discarded with the owner.

solid answer

~50 s

A flattened value is a small class with several fields that the mapping treats as *part of the owner's row*: `street`, `city` and `postcode` become three columns on the customer table, not a row in an address table. Because it has no key of its own, nothing can reference it and you cannot load, update or delete it separately — replacing it is a plain column update on the owner's row, and clearing it means writing nulls. The column names come from the mapping, not from the class, so two owners that flatten the same type need a per-use prefix or explicit overrides or their columns collide. A related shape is a collection of plain values with no identity: those live in a side table keyed by the owner's key, and layers commonly rewrite the whole set rather than diffing it.

go deeper

for a junior

Recall the shape: the value's fields become columns on the owner's row, so there is no second table and no join, and you reach the value only through the owner.

for a middle

Explain what the value concedes — no key, no independent query or delete — and that column names come from the mapping, which is why two owners of the same type need prefixes or overrides.

for a senior

Show the operational consequences: replacing the value is a same-row update, an all-null column set is ambiguous, and a side table of plain values is usually rewritten wholesale, which becomes write amplification on large sets.

for a principal

Weigh flattening against a mapped row of its own. Flattening buys locality and no join but freezes the value into the owner's table, so sharing or querying it alone later becomes a data migration rather than a mapping change.

## One object, no table of its own A **flattened value** — also called a component or an inline value — is a class with several fields (`street`/`city`/`postcode`, `amount`/`currency`, `from`/`to`) that the mapping treats as part of the **owner's row**. Each field becomes a column on the owner's table. A customer with an address is still one row in one table; loading the customer loads the address with it. No join, no second entry in the identity map, no extra statement. This is the layer's answer to a mismatch. The domain wants a small type with behaviour and equality by content; the schema wants columns it can constrain and index. Flattening keeps both: the code sees one object, the database sees ordinary columns. ## What the value concedes Because it has no key, a flattened value gives up everything identity buys: - **Nothing can reference it.** A foreign key needs a target key, and the value has none; other rows point at the owner instead. - **It cannot be loaded, updated or deleted on its own.** Every write is an update of the owner's row, and “clearing” it means writing nulls into the owner's columns. - **It has no lifecycle of its own** — no separate insert, no cascade decision, no version column; the owner's version covers it. - **It cannot be shared.** Two owners holding the same instance in memory do not share storage: each writes its own columns, and one of them mutating the shared instance quietly changes what the other will write. Treating the type as immutable and assigning a replacement removes that whole class of bug. - **Its equality is by content**, which is what makes it a value at all: two addresses with identical fields are the same address. ## Where the column names come from The names come from the **mapping**, not from the class — the detail interviews probe. Since the type's own mapping supplies the defaults, two owners that flatten the same type generate the same column names. Harmless in two different tables; fatal in one table that holds both a billing and a shipping address. Layers solve this the same way generically: a per-use prefix, or explicit per-use overrides, so the same type lands as `bill_city` in one place and `ship_city` in the other. Nesting one flattened value inside another repeats the problem a level deeper. ## Flattened value, mapped row, or side table | Shape | Own key | Queryable alone | Written by | Fits | |---|---|---|---|---| | Flattened value | no | no | the owner's update | a small value read and written with its owner | | Mapped row of its own | yes | yes | its own statements | anything referenced, shared or counted independently | | Side table of plain values | no | only via the owner's key | often a wholesale rewrite | a set or list of scalars with no identity | The third row is the collection case: tags, phone numbers, scores — values with no identity — go in a side table holding the owner's key plus the value, plus an index column when order matters. Because those rows carry no key, many layers do not diff them; they delete the owner's rows and re-insert the current contents on any change. For five values that is invisible. For five thousand it is the write amplification behind a save that got mysteriously slow. ## The edges worth knowing - **All columns null.** If nothing was ever set, layers differ on whether you get a value whose fields are all null or a null reference for the whole value. A round trip that changes the shape of the answer is a genuine source of bugs, and worth pinning down deliberately rather than discovering. - **Nullability should move as a unit.** Declaring some sub-columns not-null and others nullable makes “half an address” representable, and then every reader must decide what half means. - **Constraints and indexes still work.** These are ordinary columns: a unique constraint across the pair, a check constraint, an index on one sub-column are all available. That is the practical advantage over collapsing the value into one opaque column, where the engine can no longer see the parts. - **Reporting sees the parts.** Anyone reading the table directly sees named columns and needs no knowledge of the application's types — again unlike an opaque encoding. - **Migration is the real cost.** Turning a flattened value into a table of its own later is a data migration plus a mapping change, not a mapping change alone. Flatten when you are reasonably confident the value will never need identity; promote it when it does.

  • Two owner classes flatten the same value type. What breaks, and how is it fixed?
    Nothing breaks in the code, but the mapping generates the same column names for both, and in a single table that holds two values of the type they collide outright. The fix is per-use naming — a prefix, or explicit column overrides for that use — so the same type lands as `bill_city` on one owner and `ship_city` on another. Names belong to the mapping, not to the type.
  • How is a collection of plain values with no identity of its own stored?
    In a side table holding the owner's key and the value, plus an index column when order matters. The rows have no key, so layers commonly do not diff them: any change deletes the owner's rows and re-inserts the current contents. That is cheap for a handful of values and real write amplification for a large set.
  • When is a value better modelled as a row of its own than flattened?
    When something else must reference it, when it must be shared between owners or outlive one, or when it must be queried, counted or constrained independently. All three need identity, and a flattened value has none. Promoting it later is a data migration, so the question is worth asking while the table is still empty.

A flattened value is like a mailing address printed on an envelope rather than kept in an address book: it travels with the envelope, nothing else can point at it, and correcting it means reprinting the envelope.

saying these in an interview costs you the question

  • Says a flattened value needs its own primary key column
  • Thinks two owners can flatten the same type without renaming columns
  • Expects to query or delete the value independently of its owner
  • Assumes reading a flattened value costs an extra join
  • Believes sharing one instance between two owners shares storage
open as a page

When a converter stores an enumeration by position rather than by name, what changes and what breaks?

level: middleimportance: must knowfreq 70%

basics

~20 s

Position stores the constant's index in declaration order, so inserting or reordering constants silently re-points every existing row. The name survives reordering but breaks on a rename. An explicit, stable code declared per constant avoids both failure modes.

open as a page

A filter on an attribute written through a converter returns no rows although matching data exists — why?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Usually the predicate never went through the converter, so the object-side value was compared against the stored encoding. Set-based and hand-written statements bypass conversion entirely, and ordering compares the stored form rather than the domain's order.

open as a page

When every column backing a flattened value object is null, what can the layer hand back and why does it matter?

level: middleimportance: should knowfreq 46%

basics

~20 s

Layers differ: some return an object whose fields are all null, others return a null reference for the whole value. Either way the round trip can be asymmetric, producing null checks that never fire and updates with nothing to update.

open as a page

In a mapper with change detection, why can an in-place edit to a document-shaped converted attribute go unwritten?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Change detection compares the current state against the snapshot taken at load. If the snapshot holds the same mutable instance the code edited, both sides show the edit, the comparison finds no difference, and no update is emitted.

open as a page

How do you decide whether an attribute is converted in the layer or stored in a form the database can filter and sort directly?

level: principalimportance: nice to knowfreq 40%

basics

~20 s

Decide by who else reads the column and what the database must still do with it. If predicates, ordering, aggregation or other consumers touch the value, store a form the engine understands; reserve opaque encodings for values nothing else interprets.

open as a page