# Shopping Cart — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks ("why is that a
separate class?") and the answer that earns the mark: **what change the seam anticipates, and what
the change would cost without it.**

Thirteen classes, none over ~130 lines and most under 50. `Cart` holds the *sequence* of an
operation and no rule that a requirement could reach.

| File | One reason to change |
|---|---|
| `Cart` | the *sequence* of a cart operation changes |
| `CartFactory` | what a standard cart is made of changes |
| `CartBuilder` | how a cart is assembled changes — i.e. when a new *kind* of collaborator appears |
| `Catalog` | how the catalogue is looked up changes |
| `CartLines` | how lines are stored, ordered or windowed changes |
| `Totals` | how the money is *combined* changes |
| `Promotion`, `DiscountStage` | what a discount is made of changes |
| `PercentageCoupon` | what a percentage off means changes |
| `CouponBook` | how a code is looked up changes |
| `ShippingPolicy` / `ThresholdShipping` | what delivery costs changes |
| `Demo` | what we show when we run it changes |

## The measurement, first, because it is the argument

Three requirement changes, each absorbed with the reference's own diff measured by
`server/lib/diff.mjs`:

| Curveball | Where it landed | Measured cost |
|---|---|---|
| a second voucher that stacks, percentage first | one line in `CartFactory` + one new file | **1 line** |
| buy two teas get one free | one line in `CartFactory` + one new file | **1 line** |
| three tiers of member with their own delivery | two new files, nothing else opened | **0 lines** |

`Cart`, `CartLines` and `Totals` — the three classes that hold every operation, every line and all
the arithmetic — were **not opened by any of the three**. That is the claim the design makes, and it
is measurable rather than aesthetic.

---

## Money · integer minor units, and the one place rounding exists

**Every amount is a `long` of minor units.** No `double`, no `float`, anywhere, including in the
`record`s that cross the API boundary. This is not a style preference:

- A `double` cannot represent 0.10 exactly, so `0.10 + 0.20 != 0.30`, so a cart of ten items can
  disagree with itself about its own subtotal depending on the order the lines were added.
- Cents are a **counting** unit, not a measuring one. There is no such thing as half a cent in a
  price, so the type that can express one is the wrong type.
- `BigDecimal` would also be correct, and is heavier: it needs a scale and a `RoundingMode` at every
  operation, and it compares wrongly by default (`compareTo`, never `equals`). A `long` of minor
  units compares exactly with `==` and cannot drift. For a single-currency cart it is strictly
  better; the honest boundary is that a multi-currency system wants a `Money` type carrying a
  currency, and mixing currencies undetected is the bug that type exists to prevent.

**Multiplication is exact** — `unitPrice * quantity` of whole minor units cannot produce a fraction
— so rounding can only arise from a percentage. There is therefore exactly **one** rounding rule in
the whole design and it lives in exactly one class, `PercentageCoupon`:
`(amount * percent + 50) / 100`. That `+ 50` *is* half-up, in integer arithmetic, with no
`RoundingMode` and no floating point anywhere near it.

`long` cannot overflow for any cart a shop can physically fulfil: the largest intermediate is
`amountOwed * 100`, and a cart would have to be worth ninety million million units to trouble it.

## Seam 1 · `CartBuilder` — and the honest case for *not* using a builder

**Anticipates:** a new kind of collaborator in a cart, and any variation on the standard cart.

A cart is assembled from a catalogue, a coupon book, any number of standing offers and a delivery
rule. Three properties make a builder the right answer, and the third is the one that decides it:

1. **The required collaborator is a constructor argument.** `new CartBuilder(catalog)` — a cart with
   no catalogue cannot answer a single question, so the compiler demands one. This is the half of
   the pattern that gets left out, and leaving it out is what turns a builder into "a constructor
   you can forget to finish".
2. **The optional ones have real defaults**, each of which is a working cart: no coupons, no offers,
   no delivery charge. A caller states only what it is changing.
3. **Promotions accumulate.** `plusPromotion(...)` adds one. This is what a constructor cannot do
   well: curveball 2 added an always-on offer as **one added line at one call site**, with every
   existing construction untouched. A fifth constructor parameter would have changed the signature
   and forced every caller to restate the four things it was not changing.

**When a builder would be wrong, stated because the interviewer will ask.** For two or three
required parameters it is pure ceremony: more code, no compile-time guarantee, and a window in which
a half-built object exists. `Sku`, `LineItem` and `LineRequest` are records with no builder anywhere
near them, and giving `LineRequest`'s two fields a builder would be strictly worse. The test is not
*how many fields* but **how many can be left out, and does anything accumulate**. A builder for
three required fields is over-engineering; a builder for one required and three optional, one of
which is a growing list, is the pattern doing its job.

## Seam 2 · `Promotion` + `DiscountStage` — the order is data, not wiring

**Anticipates:** any new kind of money off. A second coupon, a multi-buy, a staff discount, a
seasonal offer, a bundle price.

Three decisions inside this one seam, each with a plausible shortcut that costs later:

**The interface is about *discount*, not *coupon*.** A `Coupon` type with `percentOff()` makes a flat
voucher a second concept, and then the totalling code has to know about both. One interface with a
pure `discountMinor(lines, amountOwed)` makes a percentage, a flat amount and a multi-buy the same
kind of thing. Curveball 1 is a new file because of this.

**The method takes the lines as well as the amount.** The narrower signature —
`discountMinor(long amountOwed)` — is enough for every discount in the base problem, which is
exactly what makes it a trap. A multi-buy is the first discount that cares *what* is in the basket
rather than *how much* it comes to. This is the one piece of genuine anticipation in the design, and
the defence is that "3 for 2" is not an exotic requirement in a shop.

**`stage()` puts the ordering on the promotion instead of in the wiring.** The requirement fixes the
order — item promotions, then percentages, then flat amounts — so encoding it is transcription, not
speculation. The shortcut is to apply promotions in the order they were registered or typed; it
passes every base test, because the base problem has one coupon, and it fails curveball 1's third
test, which types the vouchers in the reverse order on purpose. With a stage, "percentage first" is a
property of `PercentageCoupon` that no call site can get wrong. `Totals` sorts with `List.sort`,
which is stable, so within a stage the arrival order still stands — that is the only sensible
tie-break, and it is guaranteed rather than incidental.

**Why the enum has three constants when the base problem uses one.** Because the requirement names
three. The contract states the whole order up front precisely so a candidate cannot fail on an
unstated rule, and a design that mirrors the stated requirement is not speculative generality — a
design that invented a fourth stage nobody mentioned would be.

## Seam 3 · `Totals` — the cap and the order live where combining lives

**Anticipates:** any change to how discounts *combine*, as opposed to what any one discount is.

Two rules live here and nowhere else:

- **The cap.** Each discount is clamped to what is still owed. `FixedAmountCoupon` returns 500 even
  when 405 is owed, because clamping is not its business. So curveball 1's "we never pay a customer
  to shop with us" needed **no code at all**, and a fixed 500 off a 300 cart has no special case
  written anywhere. A cap inside each promotion is the same rule copied per implementation, and the
  copies drift the first time two discounts combine.
- **The stage sort.** One `sort` call is the entire ordering requirement.

**`total()` is computed on demand and nothing is cached.** A cached total in this problem has to be
invalidated by six mutators, and the seventh one added next month forgets. The cart is small, the
arithmetic is a loop over a handful of lines, and the correct answer to "isn't that wasteful?" is
that a cache here buys nothing measurable and costs a class of bug that is very hard to see.

## Seam 4 · `ShippingPolicy` — the signature encodes the rule

**Anticipates:** free-delivery promotions, per-tier delivery, weight bands, next-day charges.

Note the arguments it does **not** take: the subtotal, and the cart. It is handed the amount
**after** discount and the item count. That is the requirement — the threshold is measured on what
the customer actually pays — encoded so that an implementation cannot get it wrong, and so that a
future policy wanting the undiscounted subtotal has to change this line in the open.

The item count is what makes "an empty cart is not delivered" expressible without asking the cart
anything, which is why `ThresholdShipping` can be a pure function of two numbers.

**Cost without it:** curveball 3's silver member losing their free delivery to their own voucher is
free here. A policy handed the subtotal would need the discount passed in as well — a signature
change, and therefore an edit at every implementation and every call site.

## Seam 5 · `CartFactory` — construction is a job, and it has an owner

**Anticipates:** everything about what a cart *is*, as opposed to what it *does*. New products, new
prices, a new coupon, an offer, a delivery promotion, a tier of customer.

**`Cart` has no convenience constructor.** Every collaborator is injected and there is deliberately
no `Cart()` that supplies its own catalogue and delivery rule. All three curveballs are changes to
what a cart is made of, and none of them opens the class that validates skus, merges lines and
refuses coupons.

**The parts are public as well as the whole.** `standardCatalog()`, `standardCoupons()` and
`standardShipping()` exist so a variation reuses what it is not changing. That is the entire reason
curveball 3 costs **zero** lines: `MemberCartFactory` is a new file that borrows the catalogue and
the coupon book and swaps one policy. Without it, a member's cart either copies the five-sku
catalogue — the copy that goes stale the first time a price moves — or forces a new method into the
file every existing cart is built by.

## `CartLines` · one data-structure choice, three requirements

A `LinkedHashMap<String, LineItem>` keyed on sku code, and three of the stated requirements fall out
of it with no code written:

| The requirement said | Where it is enforced |
|---|---|
| at most one line per sku | nowhere — the sku code is the key |
| adding more of something keeps its place | nowhere — `put` on an existing key does not reorder |
| a line removed and re-added goes to the end | nowhere — the key was gone, so it is a new entry |

A `List<LineItem>` needs a scan-and-merge on every add and quietly permits two lines for one sku the
day someone forgets. A `HashMap` loses the order entirely and makes the iteration test flaky-looking
rather than failing honestly. A `TreeMap` sorts by code — a requirement nobody asked for, and one
that would have looked correct until a test asserted insertion order.

**The paging arithmetic is in `long` on purpose.** `pageIndex * pageSize` is exactly the
multiplication that overflows an `int` when a caller asks for page `Integer.MAX_VALUE`, and an
overflowed window index reads the *wrong lines* rather than failing — the worst kind of bug to have
in a pager. Both arguments are validated before anything is computed.

## D4 · the bulk and iteration shape, deliberately chosen

Four decisions, each of which had a plausible alternative:

- **`addAll` is all-or-nothing**, in two passes: resolve and merge every request, and only then touch
  the cart. The alternative — add what you can, report what you could not — is a defensible API and a
  worse one *here*, because the caller is a "buy it again" button and a half-filled basket is not a
  state a customer can reason about. Being explicit that this was a choice, and naming the partial
  variant, is the answer that earns the mark; simply saying "it's atomic" is not.
- **The merge is a `LinkedHashMap`,** which is where "repeated skus sum" and "one entry per sku, in
  first-mentioned order" both come from.
- **It returns the affected lines as they now stand,** so a caller can render the rows it just
  changed without asking again.
- **`page` returns a record carrying the window, the size asked for, the total count and whether more
  follow** — not a bare `List`. A pager that cannot say how many there are cannot draw itself, and
  "is there a next page?" computed by the caller is computed differently by each caller. A page past
  the end is **empty rather than an error**, because a stale link is a normal thing to receive.

## Errors · `IllegalArgumentException`, `IllegalStateException`, and one deliberate non-error

The contract fixes the exception types, so a domain hierarchy is constrained out here — worth saying
out loud rather than pretending the choice was free. What is left is the convention, and it is worth
defending because it is applied consistently:

- **A bad argument is `IllegalArgumentException`:** an unknown sku code, an unknown coupon code, a
  quantity that is not positive, a page that could not exist. The caller passed something wrong.
- **A bad situation is `IllegalStateException`:** the same coupon twice. The argument was fine; the
  cart was not in a state that could accept it.
- **The sku is looked up first, always.** So an unknown code is *always* "no such sku", never a
  quantity complaint, whatever else is also wrong with the call. Precedence stated once, in the
  first line of every mutator.
- **Removing more than the cart holds is deliberately not an error.** It empties the line. A shopper
  asking to take three out of a basket holding two means "take them out", and failing that request is
  pedantry dressed as rigour. This is the arbitrary rule in the problem most likely to be guessed
  differently, which is why the contract states it in bold.
- **`setQuantity(code, 0)` returns a zero-quantity `LineItem` rather than an `Optional` or a null.**
  A caller updating a row on screen always has something to render, and "the line that is now gone"
  is a real answer to "what happened?". `Optional.empty()` would force every caller to write the same
  `else` branch.

## Immutability at the boundary

Every type crossing the API is a `record` that validates in its compact constructor, so an invalid
instance cannot exist: `Sku` refuses a blank code and a negative price, `LineItem` refuses a total
that is not unit price times quantity, `CartPage` refuses a page bigger than its own size, and
`CartTotal` refuses four numbers that do not add up. That last one is the most useful: a cart whose
arithmetic disagrees with itself fails at the point of construction with a message naming the four
numbers, rather than as a mysterious expected/actual a layer later.

**`LineRequest` is the deliberate exception:** it does *not* validate its quantity, because a request
is a request rather than a promise. Refusing a zero quantity at construction would make it impossible
to ask the cart to reject a bad batch, and "the whole batch is rejected together" is the behaviour
that matters.

Collections never escape. `lines()` and `appliedCoupons()` return `List.copyOf`, `CartPage` copies in
its constructor, `Cart` copies the promotion list it is built with, `CouponBook` copies its map, and
`Catalog` never hands its map out. A caller can read everything and corrupt nothing — which matters
most for exactly the parts a candidate is expected to replace.

## Time is absent, deliberately

There is no `Clock` anywhere: nothing in this problem depends on when it happens. Worth saying out
loud rather than injecting one that nothing reads, because a speculative dependency is the same
mistake as a speculative interface. If "happy hour" or "the voucher expires on Friday" had been in
scope, the clock would be a constructor parameter of the promotion and of nothing else — which is
exactly why discounts are behind a seam.

## What was deliberately *not* built

Named so the scope is explicit rather than forgotten:

- **No stock, no reservations.** A cart is not a warehouse. Nothing sells out, so `add` has no
  "unavailable" failure mode, and the natural home for availability is a separate service the cart
  asks rather than more state inside it.
- **No checkout, no order, no payment.** Stated out of scope. The cart computes what is owed; taking
  the money is somebody else's transaction, and pretending otherwise is how a cart ends up owning a
  payment state machine.
- **No `Money` type and no currency.** One currency was stated, so a wrapper round a `long` would be
  a class earning nothing today. The honest boundary is named above: the day a second currency
  appears, `Money` is the type that stops a cart holding two of them.
- **No `Order`, and therefore no builder for one.** An `Order` with an address, a delivery slot, a
  gift message and a payment reference is the textbook builder example, and it is also a class this
  problem has no use for. Building it to show off the pattern would be inventing scope; the builder
  in this design earns its place on the cart instead, where something actually accumulates.
- **No `StackingRule` seam.** Whether coupons stack could have been a policy object. It is not,
  because the base problem has one coupon code and the requirement says nothing about limits, so the
  interface would have had one implementation and no second one in sight — the D3 single-implementation
  penalty with nothing to show for it. The measured cost of that decision is zero: curveball 1 turned
  out to need no change to `applyCoupon` at all. Had it instead asked for "at most two vouchers, and
  never two percentages", that is where the policy would go, and it would be a new file.
- **No locks.** The contract states one shopper at a time. `synchronized` on eleven methods would be
  cargo cult. The right move is to name the shared mutable state — the line map and the applied-coupon
  map, both inside one `Cart` — and say that a cart shared across threads wants a lock on the cart
  object, not on the collections, because `total()` reads both and must see one consistent basket.
- **No caching of the total.** Covered above; the invalidation is the cost, not the arithmetic.
- **No sealed hierarchy for discounts.** `Promotion` is an open interface, not a
  `sealed interface Discount permits …`. Sealing is right for a closed set of variants the *domain*
  fixes; the set of promotions a shop might invent is the opposite of closed, and sealing it would
  make every new offer an edit to the permits clause — the exact cost the curveballs measure.
