skip to content

A cart line is immutable — how do you produce one with a new quantity, and what happens to the original?

level: juniorimportance: must knowfreq 70%

answer

  1. nothing is written, something is built
  2. new value out, old value intact
  3. copy every field but the named one
  4. the return value is the whole effect
  5. rebuild outward to the cart

basics

~10 s

You build a new line that copies every field except quantity, which takes the new value. The original object is untouched, so any code already holding it keeps seeing the old quantity.

solid answer

~40 s

Because nothing can be written after construction, the change is not something you do to the line — it is a value you produce from it. A copy-with-one-field-changed takes the old line plus the new quantity and returns a fresh line whose other fields are copied across unchanged. The call has no effect of its own: if the caller drops the returned value, nothing happened. The original line stays valid and keeps its old quantity for everyone still holding it, and if the enclosing cart holds that line, the cart has to be rebuilt too before anyone reading the cart sees the change.

code

pseudocode · 9 lines
pseudocode
line = { sku: "A-12", quantity: 2, unitPrice: 300 }

function withQuantity(oldLine, newQuantity)
    return { sku: oldLine.sku,
             quantity: newQuantity,
             unitPrice: oldLine.unitPrice }

updated = withQuantity(line, 5)
// line.quantity is still 2; updated.quantity is 5

go deeper

for a junior

Recall the shape: a copy-with-one-field-changed returns a new value and leaves the old one exactly as it was. Say out loud that the caller has to use the returned value, or nothing has happened.

for a middle

Explain what the copy actually touches — one slot per field of the record, with object-valued fields re-pointed at the same objects — and why a nested change forces a new value at every enclosing level.

for a senior

Show where this bites in a running system: a component that cached the old value keeps serving it, and a reader midway through a report is unaffected. Name which holders must be handed the new value.

for a principal

Weigh what the codebase gains by making change a returned value — reviewable data flow and cheap sharing — against an API where every helper that changes something must thread its result back to the caller.

## What "changing" means when nothing can be written An **immutable** value is one whose fields are fixed at construction: once the object exists, no operation writes into it. That rules out the ordinary form of change — locate the object, assign the field, and every holder of a reference sees the new state from then on. What replaces it is **update by copy**: you build a *new* value that agrees with the old one everywhere except the part you wanted different, and hand that new value back. The consequence worth saying out loud in an interview is that the change is **not an event that happens to an object; it is a value the caller receives**. A cart line holding a quantity of 2 does not become a cart line holding 5. A second, independent line holding 5 comes into existence, and the first goes on being exactly what it was. ## The copy-with-one-field-changed shape The shape is a function that takes the value and the replacement, and returns a freshly built value: - it reads the old value's fields and writes them into a new record, substituting the one field you named; - it is a **pure function** — the old value is an argument, not a target, and nothing observable outside the function changes; - the **return value is the entire product of the call**; there is no second, hidden effect; - codebases usually expose one such function per field, or one that takes a bundle of overrides and leaves the rest alone; - the old value remains a perfectly good value and is still safe to read, pass around and keep. ## Who sees what | | writing a field on a mutable line | copying an immutable line | |---|---|---| | a holder of the old reference | sees the new quantity | goes on seeing the old quantity | | something caching the line | its cached entry silently changes meaning | its cached entry stays true to what it cached | | a reader midway through the line | may see a half-updated state | reads a value that cannot shift under it | | what the caller must do | nothing — the statement *is* the change | store or pass on the returned value | ## What is copied, and what is not A copy here is **shallow**: it writes one slot per field of the record being rebuilt. A field that holds an object is re-pointed at the *same* object — its contents are not duplicated, and no traversal of the wider graph happens. - the cost follows the **width of the record**, not the total size of everything reachable from it; - two records produced this way share every unchanged object-valued field between them; - "copy" therefore does not mean "deep copy", and quoting the cost of a deep copy overstates it badly; - the sharing is safe precisely because the shared parts cannot be written either. ## The change has to be rebuilt outward Producing the new line is only the first step, because nobody in the system holds a bare line — they hold the cart: 1. build the new line from the old one with the quantity replaced; 2. build a new cart whose line collection contains the new line in place of the old; 3. recompute whatever the cart derives from its lines — in a cart, the totals — and store them in that new cart; 4. rebind or publish the new cart wherever the old one was held, so readers reach the new value. This is why a cart that rebuilds its totals on every line-item edit is doing real work per edit, even though only one number changed. Each enclosing level that wanted to reflect the change had to be produced anew, and each carries whatever it computes from its parts. ## Where this goes wrong in practice - **Dropping the result.** Calling the copy function as though it were a command leaves the program exactly as it was; the value was built and thrown away. - **Expecting a shared holder to notice.** Another component holding the old line will keep serving the old quantity until it is handed the new value, and that is the point of the design rather than a bug in it. - **Assuming a deep copy.** Candidates who believe every reachable object is duplicated conclude that immutability is unaffordable at any size, which is a cost model for a mechanism nobody is using here. - **Forgetting the outward rebuild.** Replacing the line but not the cart leaves the cart pointing at the old line, and the totals stay right for a cart that no longer matches what the user was shown.

  • If the cart holds that line, does producing the updated line change the cart?
    No. The cart still holds the old line object. To see the new quantity through the cart you must produce a new cart whose line collection contains the updated line, and recompute whatever that cart derives from its lines. The change has to be rebuilt outward to the value your callers actually hold.
  • What must the caller do after calling a copy-with-one-field-changed function?
    Use the returned value. The call has no effect of its own, so a discarded result means nothing happened. With a mutating API the statement is the change; here the statement only computes a candidate, and the caller has to store it, pass it on or publish it.
  • Are the fields that were not changed duplicated?
    Their contents are not. The copy writes one slot per field of the record being rebuilt, and a field holding an object is re-pointed at the very same object. That is why the cost tracks the record's width rather than everything reachable from it, and why the two records share their untouched parts.

A printed price list: you do not erase a line, you print a new list with that line changed and hand it out. Whoever kept yesterday's sheet is still reading yesterday's prices, quite correctly.

saying these in an interview costs you the question

  • Says the original is updated as well once the copy is produced.
  • Discards the returned value and expects the change to stick.
  • Believes a copy-with-one-field-changed deep-copies every reachable object.
  • Claims other holders of the old value will see the new quantity.
  • Assumes the enclosing cart reflects the change without being rebuilt.