StateGENERALSTAGE-SPECIFICILLUSTRATIVE

Representation Mapping

The same cart exists four times: as UI state in a component, as JSON on the wire, as a domain object on the server, as cart and cart_items rows in a database. Knowing one concept in four representations — and what each one adds or loses — is what makes "the cart" survive a change of layer.

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

The cart's state is decided. It has to live in a React component, cross the network, be operated on by the server and sit in a database. Is that one thing or four — and what changes at each step?

The situation

The cart works as a TypeScript object in a test. Now the frontend needs it, the API needs to send it, and the database needs to store it, and you are about to write three more cart models because each layer "has its own shape". You are not sure whether they are the same cart.

The reflex

Write a cart model per layer, each fitted to what that layer finds easy: a component state with names and prices for rendering, a JSON with everything, a class on the server, a single carts table with an items JSON column. Four models, four files, and each one works in its layer.

Why it stalls

Four models that drifted apart from the start. The UI cart has names and prices (state the concept said to derive); the table has an items blob (no constraint can say "one entry per product"); the JSON has fields the server ignores. The concept was decided once and then re-decided three times by convenience.

What the reflex produces — and fails to produce
  • Four models that drifted apart from the start. The UI cart has names and prices (state the concept said to derive); the table has an items blob (no constraint can say "one entry per product"); the JSON has fields the server ignores. The concept was decided once and then re-decided three times by convenience.
  • Nothing maps between them explicitly, so every bug is a translation bug — the UI's qty against the server's quantity — and nobody can say which layer holds the truth when they disagree.
  • The database representation was chosen last and casually, and it is the one that has to be migrated. A JSON column with items in it cannot enforce the rule the concept found in State Shape From Examples; the constraint that would have caught the duplicate-row bug was never available.
  • The learner concludes that "state" means something different in each layer and has to be learned four times, when it is one concept with four encodings.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Hold one concept and map it, explicitly, into each representation. The state canvas — items of { productId, quantity } — is the source; each layer is a mapping from it, and the mapping is written down: what the layer adds (ids, timestamps, a cached name), what it cannot express (a uniqueness rule in JSON), and which layer is authoritative when two disagree.
  • Move down the ladder in order — UI state, JSON, domain object, rows — and at each level ask what this representation is *for*. UI state is for rendering and is a cache; JSON is for transport and is a serialisation; the domain object is for the operations; the rows are for survival and for the constraints that hold even against buggy code.
  • Let each representation add only what its purpose needs. The rows add a cart id, an owner column and a unique constraint on (cart_id, product_id); the UI adds looked-up names and prices for display; nothing adds a stored total, because the concept dropped it and no layer has a reason to bring it back.
  • Name the authority. The record says it plainly: "the server's cart is the truth, the UI's is a cache." That sentence decides what happens when a POST is rejected — the UI reverts — and it is the reason the rules live on the server (Where Should This Code Live?).

The ladder of representations

The same cart, four times, each level saying what it is for and why it exists. Read the because column: no level exists because a layer "has its own shape"; each exists because something — rendering, transport, behaviour, survival — needs it.

One cart, four representations
  1. UI state
    A copy of the cart in component state: entries plus names and prices looked up from the catalog for display.The screen has to render without a round trip per frame; the copy is a cache and the server's cart is the truth.
  2. JSON
    { items: [{ productId, quantity }], total } — the response of GET /cart; the request body of POST /cart/items is { productId, quantity }.Transport needs a serialisation both sides agree on; total appears in the response because the browser cannot compute it, and it is not cart state.
  3. Domain object
    interface Cart { items: CartItem[] } with addItem, removeItem, changeQuantity, getItems, total, clear.The operations need a shape they can mutate and the rules need a place to be enforced first; this is the representation the tests run against.
  4. Database rows
    cart(id, owner) and cart_item(cart_id, product_id, quantity), unique on (cart_id, product_id).The cart must survive and be found by owner from anywhere; the unique constraint holds "one entry per product" against buggy code and racing tabs.

The JSON, with what it is not

The wire representation is the one most likely to be mistaken for the concept, so it is worth looking at with its additions labelled. The total is computed for the response; the names are absent because the browser looks them up; nothing here is authoritative.

GET /cart — the transport representation
1{
2 "items": [
3 { "productId": "laptop", "quantity": 2 },
4 { "productId": "mouse", "quantity": 1 }
5 ],
6 "total": 2020
7}

total is a response field, not a stored one — the server ran the loop. No productName: the record derives it, and the UI asks the catalog. If the browser sent this back with total changed, the server would ignore it, because the server's cart is the truth.

One operation through all four

The slice shows add-item crossing every representation once. What it proves is that the mappings agree; what it does not prove is anything about concurrency, which arrives with the rows and is the first engineering problem the concept meets.

Add Laptop, end to end
The shopper clicks Add on a laptop and the cart shows Laptop × 1 after a reload.
  1. UI stateCalls POST /cart/items with { productId: "laptop", quantity: 1 } and replaces its cached cart with the response.
  2. JSONCarries the request body in and the cart response out; rejects with 400 and the rule that failed if quantity ≤ 0.
  3. Domain objectaddItem finds no existing laptop entry and appends { laptop, 1 } — the find-before-append rule enforced in code.
  4. RowsInserts one cart_item row; the unique constraint on (cart_id, product_id) would reject a duplicate the code missed.
proves
The four representations agree on the same entry, the field names survive every boundary, and the rule is enforced in code and in the schema.
does not prove
What happens when two tabs add the laptop at the same moment — the constraint catches a duplicate row, but whether the second add increments or errors is a concurrency decision the slice has not made.

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.

  • Write the mapping as a table with one row per representation: what it is for, what it adds, what it cannot express, who owns it. The gaps in "cannot express" are where a rule will need enforcing elsewhere (Rules That Live Elsewhere).
  • Keep the field names identical across representations unless a layer forces a change (snake_case in SQL). quantity is quantity in the component, the JSON, the class and the column; every rename is a translation bug waiting.
  • Put the concept's rules at the lowest layer that can hold them and repeat them upward. The unique constraint holds "one entry per product" in the database; the find-before-append holds it in the domain object; the API repeats the catalog check because it cannot trust the browser.
  • Do not let the UI representation store what the concept derives. The component may hold names and prices for display, and it should treat them as display data looked up from the catalog, not as cart state that could be sent back.
  • Walk the Implementation Ladder lab's representation mapping with the cart, then with a concept of your own; the levels are the same and the mappings differ.

Worked on a concrete problem

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

  • The record's four representations of the same cart. UI: "the UI holds a copy of the cart in component state and renders items with names and prices looked up from the catalog." JSON: GET /cart returns { items: [{ productId, quantity }], total } — total computed on the server for the response, not stored. Domain: interface Cart { items: CartItem[] } with the five functions. Rows: "cart(id, owner) and cart_item(cart_id, product_id, quantity) with a unique constraint on (cart_id, product_id)."
  • What each level adds and why. Rows add an id because the cart "needs an id once it leaves memory; in memory the variable is the identity", and an owner because "my cart has to be findable again." The JSON adds a total field to the response because the browser cannot compute it without prices — but it is a response field, not cart state. The UI adds names because it renders; none of these additions change what the cart is.
  • What each level cannot express, from the record's representations: the array "has nothing structural preventing a duplicate entry — the rule lives in the code"; the rows' unique constraint "enforces one entry per product even against buggy code." Same rule, three enforcement points, one concept.

How you know it worked

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

  • One page shows the cart in four representations with the same field names, and the differences between them are each explained by the layer's purpose.
  • You can say which representation is authoritative and what the others do when it disagrees with them.
  • The database representation carries the concept's uniqueness rule as a constraint, and you can name the bug it would catch.
  • Adding a fifth representation — a cache, a message — is a mapping exercise, not a new model.

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
  • ?What is this representation for — rendering, transport, operations or survival?
  • ?What does it add beyond the concept, and does the purpose justify each addition?
  • ?Which of the concept's rules can it not express, and where is that rule enforced instead?
  • ?Which representation is the truth, and what do the others do when they disagree with it?

What can go wrong

How the move itself fails
  • The mapping is done so faithfully that every layer has exactly the domain shape and nothing else: the UI has no names to render, the rows have no id. Each representation is allowed to add what its purpose needs; the discipline is in the reason, not in the sameness.
  • The JSON is treated as the concept. The wire format is a serialisation of the domain object for one transport; it is the easiest representation to see and the least authoritative.
  • The mapping is written and the authority is not, so the UI sends its cached names and prices back and the server stores them — the derive verdict undone by a form field.
  • The rows are designed from the JSON (an items blob) rather than from the concept (entries with a uniqueness rule), and the constraint that makes persistence safe is unavailable.
What the move costs
  • An explicit mapping is a document to maintain: when the concept gains a field, four representations and their mappings change, and the discipline of keeping them aligned is real work.
  • Identical field names across layers give up each layer's conventions — snake_case columns, camelCase JSON — or force a documented translation at the boundary.
  • Making the server authoritative costs the UI a round trip per operation before it can trust its own state; optimistic updates buy the latency back at the price of a rollback rule.
Misreads
  • "So I need a mapper library." Maybe, later. The move is knowing what each representation is for and what it adds; a mapper automates a mapping you have already understood, and hides one you have not.
  • "The database shape should mirror the domain object." The rows add identity, ownership and constraints the object never needed and lose the object's methods. Mirroring is not the goal; a justified mapping is (SQL vs NoSQL: Choosing a Data Model is the database domain's side of this).
  • "Four representations means the concept is complicated." The concept is two fields per entry. The four encodings are the cost of a cart that survives, crosses a network and is shown on a screen — engineering, not complexity in the concept (Implementation Is Not Engineering).

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.

  • GENERALAny concept that crosses layers has this ladder — a message in a chat app is component state, a WebSocket frame, a domain object and a row — and the question at each level is the same: what is this representation for?
  • STAGE-SPECIFICIn V0–V2 the cart has one or two representations and the mapping is trivial; the ladder becomes necessary at V3, when the server and the database appear, and it is easier to write then than to reconstruct later from four drifted models.
  • ILLUSTRATIVEThe qty/quantity rename, the items blob and the four files are invented to show the drift; the four representations themselves are the shopping-cart record's.

Where the depth lives

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