Your pricing core is pure functions over immutable values while the storage layer returns mutable order records - what does that seam cost?
answer
- two shapes for one concept
- the cost lives at the crossing
- copy deep enough at the boundary
- one mapping, few crossings
- pure core, mutating shell
basics
~20 sEvery crossing pays conversion: you translate between two representations of one order, copy deeply enough that neither side can edit the other's data, and keep both shapes in step whenever the model changes. Per-crossing work plus permanent duplication.
solid answer
~50 sThe seam costs three things. First **representation**: the same order exists twice, once as an editable record and once as a value, and both have to change together whenever a field is added. Second **copying**: data crossing into the immutable side must be copied deeply enough that no interior part is still shared, or the core's guarantee ends at the first alias. Third **drift**: the mapping between the shapes is code someone owns and tests. What you buy is local reasoning - past the boundary nothing can change underneath you, so results can be cached, compared and replayed. The way to keep the bill small is to make the seam thin and few: convert once at the widest point where data enters and leaves, not at twenty call sites, and never hand the shell's record straight into the core because it happens to fit.
code
pseudocode · 10 linesfunction priceOrder(orderRecord)
lines = empty list
for each line in orderRecord.lines
lines.add(makeLine(line.sku, line.quantity, line.unitPrice))
view = makeOrderView(orderRecord.id, freeze(lines))
priced = applyPricingRules(view)
orderRecord.total = priced.total
return orderRecordgo deeper
Recall that the same order can exist as an editable record and as a value that never changes, and that moving data between them means copying, not just renaming. Know that a copy which shares an inner list is not really a copy.
Explain the three costs - two representations, the deep copy at the crossing, and keeping the mapping in step - and say where you would place the conversion so the crossings stay few and countable.
Show that you have operated one: how you found a field silently defaulted by the mapping, how you kept the write-back from clobbering stored fields, and how you stopped the convenient shortcut of passing the record into the core.
Frame it as where the line between the effectful shell and the reasoning interior should fall for this system, who owns the mapping across teams, and how much interior logic justifies paying for the boundary at all.
## What the seam is A **seam** is the line where one style's assumptions stop holding and another's begin. On one side of this one sits an **immutable region**: values fully built at construction that never change afterwards, so a function that received one can reason about it for as long as it holds it. On the other sits a **mutable region**: records whose fields are assigned in place, usually because something outside the program - a store, a wire protocol, a form - owns their lifecycle and hands them back as editable state. The seam is not a design failure to be engineered away. Input and output are effectful by nature, so a pure core is always a *core*: something has to load the bytes, hand them in, and write the result back. Choosing the style per problem means deciding where that line falls, not pretending it is absent. ## The three costs you pay at a crossing 1. **Representation cost.** One concept now has two shapes: the editable record the shell reads and writes, and the value the core computes over. Adding a field to the order means touching both plus the mapping between them. This cost is permanent - it is paid every time the model changes, not every time a request runs. 2. **Copy cost.** Data entering the immutable region must be copied deeply enough that no reachable part is still shared with the shell. A shallow copy of the order that keeps the same line-item objects leaves the interior aliased, and the core's guarantee then holds only for the outermost wrapper. This cost is per crossing and proportional to the size of what crosses. 3. **Drift cost.** The mapping is ordinary code that can be wrong: a field silently defaulted, a field mapped one way but not back, a rounding that differs between the two shapes. Drift is the failure mode that survives review because each side looks correct in isolation. ## Where to put the conversion - At the **widest point** where data enters and leaves the module, so the number of crossings is small and countable. - **One direction each way**: a function that builds values from the record and a function that writes results back, rather than conversion helpers sprinkled through the call chain. - **Total, not partial**: name every field in the mapping so adding one to the model breaks the mapping loudly instead of defaulting quietly. - **Asymmetric write-back**: the core returns what changed, and the shell decides how to apply it; copying a whole freshly built record back over the stored one is how fields get lost. - **The core stays ignorant** of the shell's types. If the pure functions accept the editable record because it is convenient, the seam has moved inside the core and you are paying for a boundary you no longer have. ## The placements compared | Placement | What it costs | What it buys | |---|---|---| | One conversion at the module edge | Two shapes and one mapping to maintain | The whole interior reasons locally; crossings are countable | | Conversion at each call site | The same mapping written many times, drifting independently | Nothing structural - a model change becomes a search-and-edit exercise | | No conversion; share the editable record | No copying at all | No guarantee either: the core's results can change after it returns | ## What the cost buys - **Local reasoning.** Inside the immutable region, a value that was correct when you received it is still correct later, so a reviewer only has to read the function in front of them. - **Cheap tests.** Pure transforms need arguments, not a populated store, and a failing case is reproducible from the value alone. - **Safe caching and comparison.** A result derived from values that cannot change can be memoised or compared by content without a staleness argument. - **A named place for effects.** Bugs about *when* something was written are confined to the shell, which is usually a small fraction of the code. ## How it fails in practice - A shallow copy is treated as a defensive one, and a nested collection stays shared. - The editable record is passed into the core "just this once" for a hot path, and the exception becomes the convention. - The mapping uses defaults for unknown fields, so a newly added field reads as zero for months. - The conversion sits inside a loop that runs per line item instead of once per order, and the seam gets blamed for a cost the placement caused. - Two teams each own one direction of the mapping, and the round trip stops being lossless. The judgement an interviewer is listening for is that you priced the seam rather than denying it: you can say what crosses, how often, who owns the mapping, and what the interior gets in exchange.
- The mutable side hands you a record with nested collections inside it - where does the defensive copy belong, and how deep?At the boundary function, and deep enough that nothing reachable from the value is still reachable from the record. In practice you do not copy then wrap: you build the immutable shape field by field from the record, which makes the depth explicit and turns a missed level into a compile-or-test failure rather than a silent alias.
- How do you keep the two representations from drifting apart as the order model grows?Give the mapping one owner and one file, make it total so every field is named explicitly rather than defaulted, and add a round-trip test that converts out and back and compares. Change both shapes in the same commit; a model change that touches only one side is the drift arriving.
- When is introducing this seam not worth it at all?When the module is a thin pass-through with no real computation - it reads a record, edits a field and writes it back. There is no interior to protect, so the conversion buys no reasoning and costs two shapes. Seams pay off where there is enough logic behind them to want local reasoning.
A kitchen receiving ingredients in crates it must return decants them into its own bowls at the door. The decanting is real work, it happens once at the door rather than at every counter, and everything past the door can be trusted.
saying these in an interview costs you the question
- Calls the conversion layer pure overhead that a good compiler removes
- Passes the editable record into the pure core and calls it zero-copy
- Says making everything immutable deletes the seam rather than moving it outward
- Treats a shallow copy of a nested record as a defensive copy
- Converts at every call site, then blames the immutable style for the cost
- Writes the whole rebuilt record back over the stored one and loses fields