ConceptGENERALTEAM-SPECIFICILLUSTRATIVE

Meaning Before Representation

Do not start with classes or interfaces. A class Cart typed on the first morning fixes the representation before the data has been discovered — a stored total, a copied product name, an item type nobody challenged. Discover what the cart must remember, what can happen to it and what must stay true; the class, if there is one, is the last thing written and the easiest to change.

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

Why not just write the class? What does typing class Cart { items: Item[] } on the first morning decide, and what would you have found out if you had waited?

The situation

You have the sentence and a rough list of nouns. Your editor is open. Every instinct — and every tutorial — says the next step is interface CartItem and class Cart. Typing them feels like the first real progress of the day.

The reflex

Write the types first. interface CartItem { product: Product; price: number; quantity: number }, class Cart { items: CartItem[]; total: number }. Now the compiler will tell you what is missing.

Why it stalls

The compiler tells you what is inconsistent, never what is unnecessary. total is now a field that has to be updated in every method, and price is a copy that goes stale when the catalog changes; both typecheck perfectly.

What the reflex produces — and fails to produce
  • The compiler tells you what is inconsistent, never what is unnecessary. total is now a field that has to be updated in every method, and price is a copy that goes stale when the catalog changes; both typecheck perfectly.
  • items: CartItem[] has chosen the array before anyone asked what the operations need. The choice happens to be right for a cart — and it was made for no reason, so when the same reflex meets a wishlist with thousands of entries it makes the same choice again, wrongly.
  • The class arrives with no operations. It has fields and a constructor, and addItem gets written last, by looking at the fields and guessing what to do with them — so the second add appends, because nothing in the fields says otherwise.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Hold the representation open until the data has been *discovered*, not declared. The order is meaning → state → operations → rules → examples → representation → code, and the reason is that each step is evidence for the next: the operations tell you which fields are read, the rules tell you which structure makes them hold, the examples tell you what a field must be able to express (The Implementation Loop).
  • Discover the state by challenging every candidate field (Challenging Unnecessary State). Ask of each: does an operation read it? Can it be computed? Who owns it? The record's canvas keeps items, productId, quantity; derives productName; marks price and owner as depends; drops total and createdAt. A class typed first would have kept all eight.
  • Let the rules and the operations choose the structure, not the habit. "Find the existing entry before appending" is an O(n) scan over an array and an O(1) average lookup in a map; "render in the order added" is free in an array and language-dependent in a map. The record picks the array *because* a cart holds a handful of items and is shown in order — a reason someone can revisit at a different size (Array Cart vs Map Cart).
  • Write the class last, and small. interface CartItem { productId; quantity }, interface Cart { items: CartItem[] } — two lines, each field with a reason on the canvas, the structure with a reason in the representation note. It is the easiest thing in the file to change, because everything above it is what it was derived from.

One order, and the other

The module's order, with the reason each step precedes the next. The alternative is the one most experienced engineers actually use on familiar ground, and it is legitimate there — the lesson is not that types-first is always wrong, it is that on an unfamiliar concept it decides too much too early.

Meaning → … → representation → code
  1. 1
    Meaning: one sentence

    because Everything else is read off it; a representation chosen before it has nothing to be checked against.

  2. 2
    State, challenged field by field

    because The operations need something to read, and the challenge column is where borrowed fields — total, price, name — are caught.

  3. 3
    Operations, in plain English

    because They say which fields are read and changed, which is the evidence the representation choice needs.

  4. 4
    Rules and examples

    because "One entry per product" and "render in order" are what make array against map a decision instead of a habit.

  5. 5
    Representation, with a because

    because Chosen for the reads, the rules and the size you have; written down so a different size can revisit it.

  6. 6
    Types and code

    because Two lines copied from the keep column; the easiest thing in the file to change, because it was derived.

a different valid order Types first, on familiar ground: when you have built the concept several times, write interface Cart { items: CartItem[] } immediately and let the examples and rules correct it. Choose this when the canvas is already in your head and the cost of a wrong field is a one-line edit — and notice that it is exactly the reflex the lesson warns against, made safe by experience the learner does not yet have.

The first-morning class against the discovered type

Both compile. The difference is the six fields that the canvas removed or deferred and the one structural choice that gained a reason. Every removed field in the left column is a real bug the record lists — a stale price, a copied name, a total to keep in sync.

Typed first
interface CartItem {
  product: Product      // a copy of the catalog's product
  price: number         // goes stale when the catalog changes
  quantity: number
}
class Cart {
  items: CartItem[] = []
  total = 0             // must be updated by every method
  owner: string         // V0 has one cart and no owner
  createdAt: Date       // read by nothing
}
Declared after the canvas
type ProductId = string
interface CartItem { productId: ProductId; quantity: number }
interface Cart { items: CartItem[] }

// items[]     keep    — the cart is its items
// productId   keep    — a reference; the catalog owns the product
// quantity    keep    — "add again" needs a count
// productName derive  — from the catalog, never stored
// price       depends — derived for a cart, snapshotted for an order
// total       drop    — computed: a loop over items and priceOf
// owner       depends — arrives with V1, two shoppers
// createdAt   drop    — no V1 operation reads it
// items: array — a handful, rendered in order; O(n) find is invisible

The typed-first class has decided eight things and can defend none; the declared-after type has decided three and carries the reasoning for the other five as comments that will become code when a version needs them. The array is chosen in both — only one of them knows why.

What each representation costs each operation

The representation is chosen last because this table cannot be filled in until the operations are known. Complexity is annotated from the record, not asserted; the caveat is what keeps "array" from becoming a habit.

Array against map, for the cart's operations
OptionSimplicityPerformanceReliabilityMaintainabilityNote
Array of CartItemEvery language has one and it serialises to JSON for free; add is an O(n) scan and remove an O(n) filter; duplicates are prevented by the code only; insertion order is preserved, so the cart renders as added.
Map from product idAdd and remove are O(1) average and "one entry per product" is structural — the key cannot repeat; serialising needs a conversion step and order depends on the language.

caveat For a handful of items the O(n) is invisible and the array wins on every other row; for a wishlist with thousands of entries, or a server holding many carts keyed by owner, the map's rows win. The table is a function of size and of which rule you want the structure to hold — not a ranking.

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 any type, fill the state canvas: field, type, why, challenge, verdict. A field without a challenge column is a field nobody tried to remove (What Must It Remember?).
  • Write the operations in plain English against the *canvas*, not against a class; if an operation needs a field the canvas does not have, add it to the canvas with the operation as its reason (What Can Happen to It?).
  • Write the rules and ask which structure would make each one hold by construction; then choose the structure for the rules and the reads you actually have (Choosing a Representation).
  • Only then declare the types, copying the canvas's keep column and nothing else. Compare the declared type with the class you would have typed on the first morning and count the differences.
  • If a framework or ORM demands a class shape first, write the domain type anyway and map to the framework's shape at the boundary (Representation Mapping).

Worked on a concrete problem

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

  • First-morning class: CartItem { product: Product; price; quantity }, Cart { items; total; owner; createdAt }. After the canvas: CartItem { productId; quantity }, Cart { items }. Six fields gone or deferred, each with a written reason — the name derived from the catalog, the price derived for a cart and snapshotted for an order, the total computed, the owner from V1, the timestamp from V6. The compiler would have accepted all fourteen.
  • Representation. The operations read items by product id (add, change, remove) and in order (view); the rule "one entry per product" wants a find before an append. Array: O(n) find, order free, JSON for free. Map: O(1) average find, uniqueness structural, order language-dependent, a conversion step to serialise. For a handful of items rendered in order: array, with the note that a wishlist with thousands of entries would flip it.
  • Search products by name, for contrast. The first-morning representation is products: Product[] and a filter. The discovered state is the same array — and the operation "match by name" over a large catalog is O(n) per keystroke, so the representation question is really an index question that the first morning would have skipped (Why This Data Structure?).

How you know it worked

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

  • The types are the last thing in the file, are shorter than the first-morning version, and every field in them appears in the canvas's keep column.
  • The structure has a because that names the operations and the size it was chosen for.
  • Changing the representation — array to map — touches the find and the append and nothing about the meaning, state, rules or examples.

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
  • ?Which fields in the class I was about to type would survive the challenge column — and what is my evidence for each?
  • ?Which operations read this field, and which rule needs this structure?
  • ?At what size, or for which requirement, would the structure I chose be wrong — and did I write that down?
  • ?If I changed the representation tomorrow, which lines would move?

What can go wrong

How the move itself fails
  • The canvas is filled in and then the first-morning class is typed anyway, out of habit, with total back in it. The canvas has to be the thing the types are copied from.
  • Representation is deferred so long that no example can be run; a canvas with no code for a week is a design document, not a discovery. The array is a fine default to try examples against, as long as it is labelled a default.
  • Meaning-before-representation is applied to a concept the learner has built five times, and they spend a morning rediscovering that a cart is a list.
What the move costs
  • Discovering the data before declaring it delays the first compiling file by hours on a new concept and buys nothing on a familiar one.
  • A type declared last is easy to change and late to catch mistakes; the compiler finds inconsistencies only once there is something to compile.
  • Choosing the array "for now, with a reason" is right and produces an O(n) scan that a later engineer may see and "fix" to a map without reading the reason.
Misreads
  • "Don't start with classes" means never use classes. The class is fine; the order is the point. A class written after the canvas is two lines with reasons; the same class written first is eight fields with none.
  • "Types are documentation, so writing them first is designing." Types document what is declared, not what was discovered; total: number documents a decision nobody made. Writing types first is precise only if the state was discovered somewhere else — a canvas, examples — and the types are its transcription.
  • "The compiler will tell me what is missing." It tells you what is inconsistent with what you declared. It has no opinion on a stored total, a copied price or a field no operation reads — the three mistakes the canvas exists to catch.

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.

  • GENERALDiscover state, operations and rules before declaring a representation applies to every concept in the track; the class-first reflex is the same for a cart, a rate limiter and a job queue, and so is the field it smuggles in.
  • TEAM-SPECIFICA team fluent in the domain may go from sentence to types in one sitting because the canvas is in their heads; a learner or a team on an unfamiliar concept needs the canvas on paper, because the challenge column is where the borrowed fields are caught.
  • ILLUSTRATIVEThe eight-field class, the handful of items and the wishlist with thousands of entries are invented to make the differences countable; no real store or catalog size is described.

Where the depth lives

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