In a data-access layer, what does it mean to flatten a multi-field value object into its owner's columns?
answer
- part of the owner's row
- no key, no independent lifecycle
- column names come from the mapping
- two owners of one type collide
basics
~20 sFlattening 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 sA 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
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.
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.
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.
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