Where Should This Code Live?
Frontend, backend, database or shared library — the cart's five functions and five rules can live in any of them, and the reflex puts them wherever the current file is. Authority, security, persistence, reuse and latency decide, and the same rule often lives in two places for two different reasons.
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 cart's logic exists once. Should it run in the browser, on the server, in the database, or in a library all three import — and what decides, other than where you happen to be typing?
The cart runs in the browser because that is where it started. Now checkout needs the total, the total must be right, and the product page shows "in stock" from a number the browser fetched a minute ago. You copy addItem into the server. Now there are two addItems, and when the maximum-quantity rule arrives you fix one of them.
Put the logic where the current file is. The button is in the component, so the rule goes in the component; the endpoint is on the server, so the rule is copied there; the constraint is in the migration, so it is written a third time. Each placement is locally sensible and nobody made the decision.
The rule exists three times and drifts. The browser rejects quantity 6 with a message; the server accepts it because that copy was never updated; the database has no constraint. The maximum is enforced exactly where the shopper can bypass it and nowhere they cannot.
- The rule exists three times and drifts. The browser rejects quantity 6 with a message; the server accepts it because that copy was never updated; the database has no constraint. The maximum is enforced exactly where the shopper can bypass it and nowhere they cannot.
- The total the customer pays is computed by code the customer runs. A modified request sends its own total and the server, which computed nothing, stores it. Nobody decided the browser was authoritative over money; the file layout did.
- The stock check happens in the browser against a number that was true when the page loaded. Two shoppers both see "in stock", both add, and the last unit is sold twice — and the code that could have known lived on the server, where nobody put the check.
- A background job that merges carts at login imports nothing, because the cart logic is inside a React component. The job reimplements addItem and forgets the find before the append; Predict the Bug appears in production.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- For each rule and each operation, ask five questions and let the answers place it. Authority: whose answer must win when the copies disagree? Security: can the place it runs be tampered with by the person it constrains? Persistence: does the rule have to hold against every writer, including buggy ones? Reuse: who else will call this — a job, a second client, a test? Latency: does the shopper need the answer before a round trip could return it?
- Expect the same rule in two places for two different reasons. "quantity > 0" runs in the browser for immediate feedback (latency) and on the server for truth (authority, security). That is not duplication of a decision; it is one rule with two enforcement points, and the server's verdict wins — the concept record says this of the catalog check.
- Reserve the database for rules that must hold against every writer. "One entry per product" as a unique constraint is the last line of defence: it holds when the server has a bug, when a job bypasses the service, and when two tabs race. It cannot hold the maximum-quantity rule the same way without a check constraint, and it cannot ask the catalog at all — so what lives there is the small set of invariants that are structural.
- Write the pure logic once in a place all three can import, and let each layer decide what to enforce from it. The functions do not know which layer called them; the layers know which rules they are responsible for. When the rule changes, it changes in one file and the enforcement points pick it up — the cost is that the library must be genuinely free of browser and server assumptions (Framework Independence).
Five questions place a rule
The decision is per rule and per operation, not per concept. The criteria below are what to ask; the cart record's own "where it lives" entries are the options. Notice that the options are not exclusive — most rows in the worked examples ended in two columns.
For one rule or operation: authority, security, persistence, reuse, latency — which combination of places satisfies all five?
when The browser-storage rung: no login, one device, and the total shown is advisory until checkout recomputes it. Also: a check whose only purpose is instant feedback.
cost Nothing here can be trusted by anyone but this browser; the moment the server must trust the cart, this column alone is insufficient.
when The moment anything about the cart must be trusted — stock, prices, the total that becomes an order. Authority and security put the rules on the server.
cost A round trip per check; feedback arrives after the request returns unless a client copy exists too.
when The same validation runs in the browser for fast feedback and on the server for truth; the rules are written once, the server's verdict wins.
cost A module that imports nothing environment-specific, and a build that both sides can consume.
when The uniqueness rule as a constraint — the last line of defence against a buggy client or a race.
cost Only structural invariants fit; anything needing the catalog or inventory cannot live here, and a violation surfaces as a database error rather than a named rule.
One rule, three enforcement points
The rule below is the record's first rule. It is the same sentence in all three places; what differs is the reason each place has it and what happens when only that place catches it. The pseudocode is the shared function's check — the browser and the endpoint both call it — and the constraint is the database's version of the same sentence.
rule Every quantity in the cart is greater than zero.
↓ becomes validation Reject an add or change whose quantity is ≤ 0; turn a change to 0 into a removal. Run in the browser so the form can say so immediately; run on the server because the request may not have come from the form; hold in the database so that no row can violate it even if both copies are wrong.
-- shared function, called by the form and by the endpoint
if quantity <= 0:
reject "quantity must be positive"
-- database: the structural version of the same sentence
cart_item.quantity INTEGER NOT NULL CHECK (quantity > 0)The shape that makes sharing possible
The diagram shows the placement the worked examples arrived at: one module of pure functions and rules, imported by the browser for feedback and by the server for truth, with the database holding the structural invariant beneath both. The arrows are import and call relationships; the note is the property that keeps the picture true.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- 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.
- 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.
- 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
- • 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.
- Draw a grid: rows are the rules and operations from the concept record, columns are frontend / backend / database / shared library. Mark each cell "enforces", "computes", "caches" or blank; a rule with only a frontend mark is the one to worry about.
- For anything involving money, stock or ownership, the backend column is required — the client cannot be authoritative over what it is constrained by (Source of Truth).
- For any rule that must survive a buggy writer, ask whether the database can express it structurally; if it can, add the constraint and keep the code check too.
- Put the five functions in a module with no imports from a framework, a database driver or the DOM; that is the test of whether they can be shared.
- When latency argues for a client-side check, keep it as a copy of the shared rule, not a reimplementation (Duplicate Knowledge in Software Design names the failure).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The rule "every quantity is greater than zero". Authority: the server must reject it regardless of what the form allowed. Security: a request can carry −3. Latency: the shopper should see the rejection as they type. Placement: the shared function, imported by the form for instant feedback and by the endpoint for truth; the database gets a check constraint quantity > 0 because a bad row must not exist even if both copies are wrong.
- The operation "total". Authority: the amount that becomes the order must be the server's. Latency: the cart page wants a running total without a round trip. Placement: the shared function, run in the browser for display and on the server for the order; the browser's number is advisory and the record says so — "the total shown is advisory until checkout recomputes it".
- The rule "one entry per product". In code, it is the find before the append, shared. Persistence: two tabs racing on a database cart can both take the append branch; only the database sees both writes. Placement: the shared function plus a unique (cart_id, product_id) constraint — the record calls the constraint "the last line of defence against a buggy client or a race".
- The stock rule, once V5 arrives. Authority: inventory owns stock, not the cart. Placement: not the browser (stale), not the cart's own code alone (it does not own the number), but a call from the server-side add to the inventory service — and re-asked at checkout, because the record says enforcing it at add alone "is a race the inventory service has to resolve at checkout".
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every rule has a written reason for each place it runs, and the reasons differ — latency in the browser, authority on the server, structure in the database.
- The pure functions import nothing from a framework, a driver or the DOM, and are called from at least two layers.
- Nothing involving money, stock or ownership is decided only by code the shopper runs.
- When a rule changes, you can name the one file that changes and the enforcement points that pick it up.
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.
- ?For this rule, whose answer must win when the browser and the server disagree?
- ?Can the place this check runs be modified by the person it constrains?
- ?Must this invariant hold against every writer, including a buggy one — and can the database say it structurally?
- ?Who else will call this logic, and can they import it from where it lives now?
What can go wrong
- Everything goes to the server for safety. The form cannot say "quantity must be positive" until a round trip returns, every keystroke is a request, and the shared-library option — the same rule, run early for feedback and late for truth — was never considered.
- Everything goes to the database. Business rules become triggers and stored procedures, the tests from the examples cannot run without the database, and the catalog check, which needs another system, cannot be expressed there at all.
- The shared library grows framework assumptions. A helper reads from the DOM or a request object, the library cannot run in the job that merges carts, and the "shared" code is quietly two copies again.
- The grid is drawn once and never revisited. V3 moves the cart to the server and the browser copy, which was authoritative on the browser-storage rung, stays authoritative in the shopper's mind and in the code that still writes to storage first.
- A shared library costs a build step that produces something both the browser and the server can import, and a discipline that keeps framework code out of it.
- Enforcing a rule in two places doubles the places a change must be verified, even when the code is shared; the constraint in the database is a third.
- Client-side checks for latency give a faster form and a second implementation surface that must match the server's verdict exactly, including its error message.
- "Never duplicate a rule." A rule enforced in the browser for feedback and on the server for truth is one rule with two enforcement points, and the falsifiable version of the slogan is: never let two copies of a rule be maintained separately.
- "Backend logic is safer, so put everything there." Safer against tampering, yes; also a round trip per check. Authority goes to the server; feedback can stay in the client as long as the server's verdict wins.
- "The database constraint means the code check is redundant." The constraint catches what the code missed and reports it as a database error; the code check reports it as the rule the shopper broke. Both exist for different readers.
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.
- GENERALAuthority, security, persistence, reuse and latency are the placement questions for any logic that a client and a server both touch — a chat message's length limit, a file upload's size limit, a search query's validation.
- TEAM-SPECIFICA team with one language across browser and server can share the functions directly; a team with a Python backend and a TypeScript frontend shares the rule in words and tests both copies against the same examples, which is the same discipline at higher cost.
- ILLUSTRATIVEThe quantity −3 request, the maximum of six and the doubly-sold last unit are invented to show the placement questions; no real incident is described.
Where the depth lives
This domain asks the question and hands the answer off by name.