RepresentationCONTESTEDTEAM-SPECIFICILLUSTRATIVE

Multiple Correct Solutions

The array cart and the map cart both pass every test; so does a cart that filters instead of finds. A reference solution is not the right answer — it is one answer whose trade-offs are explained, and the ability to say why yours differs is the understanding.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

Your implementation differs from the reference. Is yours wrong, is the reference wrong, or are you looking at two correct answers — and how would you tell?

The situation

I finished the cart, ran the tests, and then opened the reference solution. It uses a map; mine uses an array. It throws on removing a missing product; mine does nothing. It has a findItem helper; I inlined the loop. Every difference feels like a mark against me, and I cannot tell which differences are mistakes, which are choices, and which are the reference's own taste.

The reflex

Converge on the reference. Rewrite until the diff is empty — the map, the throw, the helper — because the reference was written by someone who knows, and matching it removes the discomfort of not knowing which differences matter. Alternatively, the defensive reflex: decide every difference is style and learn nothing from any of them.

Why it stalls

Converging erases the one thing the comparison could have taught: which differences were rules and which were choices. After the rewrite the code matches and the learner still cannot say why removing a missing product should throw or not — they can only say that the reference did.

What the reflex produces — and fails to produce
  • Converging erases the one thing the comparison could have taught: which differences were rules and which were choices. After the rewrite the code matches and the learner still cannot say why removing a missing product should throw or not — they can only say that the reference did.
  • The reference's choices are inherited as rules. Its map becomes "carts are maps"; its throw becomes "removal must fail on absence"; the next concept is built to those rules, and the trade-off the reference made for its own reasons is now the learner's constraint without the reason.
  • The defensive reflex loses the real mistakes among the style. Somewhere in the diff is a difference that is a bug — the inlined loop that keeps scanning after the match, or a merge that skips the uniqueness check — and "it is all just style" hides it beside the differences that really were.
  • The idea that there is one right data structure takes hold, and with it the anxiety that every choice could be the wrong one. That anxiety is what makes people ask the AI for the answer instead of deriving one, because an answer received cannot be wrong in the way a choice made can.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Sort every difference into one of three bins before judging any of them: behaviour, trade-off, or style. A behaviour difference means the two versions give different after-states for some input — that is a disagreement about the rules, and exactly one of them is right per the rules you wrote. A trade-off difference means the same behaviour by a different path with different costs. A style difference means the same behaviour, the same costs, different text.
  • For behaviour differences, go back to the examples and rules, not to the reference. "Remove an absent product" — does the concept say error or no-op? The shopping-cart record chose no-op, with a reason; if your version throws, you disagree with the rule, and the rule is the authority, not the reference file. If the rules do not say, you have found a rule that needs deciding (Normal, Edge, Invalid).
  • For trade-off differences, run the comparison you would run for any representation: what does each cost, on which operation, at what size, and what does each make structural. Both are correct; one is better for a stated reason. Write the reason, and you have understood the reference better than by copying it (Array Cart vs Map Cart).
  • For style, decide once and stop. A helper or an inlined loop, a filter or a splice — the code does the same thing at the same cost. Style is worth a team convention and not worth a lesson; the danger is spending the comparison's attention on it.

Two correct addItems

Both functions below pass every test the concept derives from its examples. They differ in representation and in how the "one entry per product" rule is kept — as a find in one, as a key in the other. The device labels them worse and better because it has to; the lesson does not, and the because says what each is better at.

Read them for the line that differs. It is the same line in both — the lookup — and everything around it is the operation, unchanged. A diff that shows only that line is a trade-off; a diff that changes the branch or the return would be a behaviour difference and would need an example to settle it.

The same operation, two defensible implementations
Array cart — the concept record's choice
function addItem(cart, productId, quantity = 1):
    if not catalog.has(productId): reject "unknown product"
    if quantity <= 0: reject "quantity must be positive"
    item = find entry in cart.items with item.productId == productId
    if item exists: item.quantity = item.quantity + quantity
    else: append { productId, quantity } to cart.items
    return cart
Map cart — a reference that chose the other bound
function addItem(cart, productId, quantity = 1):
    if not catalog.has(productId): reject "unknown product"
    if quantity <= 0: reject "quantity must be positive"
    item = cart.items.get(productId)
    if item exists: item.quantity = item.quantity + quantity
    else: cart.items.set(productId, { productId, quantity })
    return cart

Neither is better in general. The array keeps insertion order and serialises to JSON without a step, and is the better answer for a cart bounded by a shopper; the map makes uniqueness structural and its lookup does not grow, and is the better answer for a large collection or one held server-side by key. A learner who can say that sentence has understood both; one who rewrote to match has understood neither.

What every correct solution must satisfy

The way to tell a trade-off from a bug is to know what all correct solutions have in common. The tree below decomposes "a correct cart" into the observations every implementation must pass, whatever its structure. A difference that fails a leaf is a behaviour difference; one that passes every leaf is a trade-off or style.

Note that the leaves come from the concept's examples and rules, not from any implementation. That is what makes them the authority: the reference is measured against them too.

A correct cart, representation-independent
Any correct cart implementation
  • Keeps the rulesthe invariants are the contract
    • One entry per producttestable Add Laptop twice from empty; exactly one entry, quantity 2 — the add-again example.
    • Every quantity positivetestable Add with quantity 0 is rejected and the cart is unchanged; change to 0 removes the entry — invalid-zero and to-zero.
    • Unknown products rejectedtestable Add a product id not in the catalog; rejected with a reason, cart unchanged.
  • Matches the examplesbehaviour is defined on concrete values
    • Every before/operation/after in the concepttestable Running the six examples produces the six after-states, in either representation — the Cart Lab's agreement check.
    • Decided edge casestestable Remove an absent product gives the outcome the rule chose (no-op in V1), not whichever the structure made easy.
  • Exposes the operations, not the structureso the representation can change
    • Callers use add / remove / change / view / totaltestable Swapping the array for the map changes no caller and no test — only the five functions.

A reference solution and your solution both hang from this root. The leaves are where they must agree; everything else is where they are allowed to differ, and the differences there are the trade-offs worth explaining.

A third correct solution, and what it costs

To make the point concrete: here is a cart whose remove is written with a filter that rebuilds the array, beside a mental version that splices in place. Both pass every leaf above. The filter allocates a new array on every removal and leaves the old one for the collector; the splice mutates and saves the allocation. At cart size the difference cannot be measured; in a hot loop over a large collection it can.

The comment is the point of the section. It states the trade-off in the code's own favour and names when the other version wins, which is what a reference solution owes its reader and what your solution owes the next one.

removeItem by filter — correct, with its trade-off stated
1export function removeItem(cart: Cart, productId: ProductId): Cart {
2 // Filter rebuilds the array: O(n), one allocation, no index bookkeeping,
3 // and removing an absent product is a no-op for free (the V1 rule).
4 // An in-place splice avoids the allocation and wins only when removals
5 // are frequent on a large collection — not a cart. Both pass every example.
6 cart.items = cart.items.filter((i) => i.productId !== productId)
7 return cart
8}

The reference and this version agree on every after-state, which makes the difference a trade-off; the comment says which way it goes at cart size and which requirement would flip it. That sentence is what "understanding the solution" means, and it is available to whoever wrote either version.

The implementation ladder

Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.

concept Shopping Cart beginner
Build it step by step →

Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.

Identity, ownership, lifetime
  • Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
  • Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
  • How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
  • Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
  • Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
State it must remember
  • itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
  • items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
  • items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
  • owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
  • items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
  • items[].pricemoneydependsThe total needs a price per item.
  • totalmoneydropEvery screen shows the total.
  • currencycodedependsPrices need a currency to be added.
  • createdAttimestampdropAbandoned carts might be expired or emailed about.
Operations
  • update Add item the updated cart
  • delete Remove item the updated cart
  • update Change quantity the updated cart
  • read View items the list of entries — product id and quantity — for rendering
  • domain Calculate total the sum of price × quantity over the entries
  • delete Clear cart the empty cart
Rules that must always hold
  • Every quantity is greater than zero.
  • One logical entry per product.
  • The total is never negative.
  • An unknown product cannot be added.
  • Quantity cannot exceed available stock — if inventory is enforced here.

How to do it

Most important first.

  • Before reading the reference, write your own solution's trade-offs: which structure, why, what it costs, what would flip it. The comparison is between two explained solutions, not between yours and the truth.
  • Diff, then bin. For each difference write B, T or S. A B needs an example that distinguishes the two; if you cannot construct one, it is not a B (Examples Become Tests).
  • For each B, find the rule in the concept that decides it. If there is none, the rule is missing, and deciding it — in words, then as a validation, then as code — is the real output of the comparison.
  • For each T, write one sentence per side: what it costs and when it wins. If you cannot write the sentence for the reference's side, you have not understood its choice; if you cannot write it for yours, you made it by accident.
  • Keep the version whose trade-offs you can defend, which is sometimes yours. A reference solution that explains its trade-offs is asking to be argued with; one that does not is a tutorial (Before You Copy Code).

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • Diff one: the reference uses a map; mine uses an array. Bin: T. Both pass the add-again example with [ Laptop × 2 ]. Array: O(n) scan on add, insertion order and JSON for free; map: O(1) lookup, structural uniqueness, a conversion at the boundary. The concept record itself chose the array and says why; my version matches the record, and the "reference" was another correct answer with a different bound in mind.
  • Diff two: the reference throws on removing an absent product; mine returns the cart unchanged. Bin: B — the example [ Laptop × 2 ], remove Keyboard gives an error in one and [ Laptop × 2 ] in the other. The rule decides: the concept says "V1 chooses nothing happens, because removing something absent leaves the cart in the state the caller wanted", and notes that an API might choose 404. Mine matches the rule; the reference made a different rule choice that its own trade-offs should have stated.
  • Diff three: the reference has a findItem helper; mine inlines for … if … return. Bin: S — same behaviour, same cost. Except that on closer reading my loop does not return early and keeps scanning after the match: still the same result, twice the work, and a habit that becomes a bug when the loop mutates. Binned as S, it turned out to hide a T, which is why each bin gets checked with an example rather than a glance.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • Every difference from the reference is labelled behaviour, trade-off or style, and each behaviour difference has an example that distinguishes the two versions.
  • Behaviour differences were settled by the concept's rules and examples, not by which file was called the reference — and at least once the rule sided with you.
  • You can state the reference's trade-off in its own favour, and yours in yours, and say which bound or requirement would make each the better one.
  • The phrase "the right data structure" has been replaced, in your own speech, by "the structure that fits these operations at this size".

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?For this difference, can I construct an input on which the two versions give different after-states?
  • ?If they differ in behaviour, which rule or example decides — and does one exist?
  • ?If they differ in trade-off, what does each side cost, and under what bound does each win?
  • ?Can I state the reference's choice in its own favour, as its author would?
  • ?Is there a difference I binned as style that an example would reveal as something else?

What can go wrong

How the move itself fails
  • Every difference is binned as a trade-off, so a real bug — a merge that skips the uniqueness check — is defended as a choice. A trade-off has the same after-state on every example; the bug does not, and the example is the test.
  • Every difference is binned as behaviour and litigated against the rules, including whether the helper is named find or findItem. The bins exist so that style stops costing attention; a comparison that spends an hour on names has lost the purpose.
  • The move is used to dismiss the reference. It was written by someone who thought about the trade-offs, and its choices carry information — the throw on removal is a real position that an API layer holds. Disagreeing with a reason is the goal; disagreeing to feel independent is the defensive reflex with vocabulary.
  • The concept's rules are treated as settled when the comparison shows they are not. If the reference and your version disagree on a case the examples never covered, the finding is a missing rule, and moving on without deciding it leaves the disagreement in the code.
What the move costs
  • Binning and checking every difference is slower than converging, and for a beginner's first cart most of the differences really are style; the method pays off on the two that are not.
  • Defending your own version means carrying a solution that differs from the reference, and in a team the reference may be the convention — correct and different is sometimes correct and rejected.
  • Treating a disagreement as a missing rule reopens the concept's rules after the code is written, which is the right time to find the gap and the wrong time for the schedule.
Misreads
  • "Any solution that passes the tests is as good as any other." Any such solution is correct on the examples the tests came from; the trade-offs still differ, and the tests say nothing about what happens at a bound they did not exercise. Correct is the entry condition, not the verdict.
  • "The reference is just one opinion." It is one explained answer, and the explanation is what makes it worth more than an opinion. A reference whose trade-offs you have read and can restate is a second engineer you argued with; ignoring it is ignoring the argument.
  • "Since there are many correct solutions, the choice does not matter." It matters exactly as much as the stated trade-off says, which for a cart is little and for a server-side index is the design. The lesson is that the choice needs a reason, not that it needs none.

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • CONTESTEDSome experienced engineers hold that for a given well-specified operation there is a best data structure and that teaching "several are correct" excuses a learner from finding it: the operations and the bound determine a dominant choice often enough that the honest lesson is "derive the best one and know why the others lose", and a learner told that array and map are both fine will stop before the analysis that shows which one wins here. The position this lesson takes is that the analysis is the same either way — cost per operation, at the bound — and that the useful output is the stated trade-off, which sometimes shows a dominant choice and sometimes shows two that tie at this size; presenting the tie as a loss teaches the anxiety that sends learners to the AI for the answer.
  • TEAM-SPECIFICA solo learner keeps whichever version they can defend; on a team the reference may be the convention, and a correct, well-argued, different solution is still rewritten to match — the argument is worth having, the rewrite is still done.
  • ILLUSTRATIVEThe three diffs — map versus array, throw versus no-op, helper versus inline loop — are invented from the shopping-cart record to show the three bins; no specific reference implementation is being graded.

Where the depth lives

This domain asks the question and hands the answer off by name.

Further
  • The manifesto's review page at /manifesto/review is the same move applied to an AI's answer: bin its differences from your attempt into behaviour, trade-off and style before accepting any of them.
  • The Build Without AI route at /manifesto/without-ai reveals a reference last for this reason — a reference read first has nothing to be compared against.