OperationsGENERALCONTESTEDILLUSTRATIVE

CRUD and Domain Actions

Sort the operations into CREATE, READ, UPDATE, DELETE and DOMAIN ACTIONS. The cart fits mostly into CRUD with "calculate total" as a domain action — but "reserve stock" and "check out" are domain actions that CRUD would mangle, and not every domain should be modelled as four verbs on a table.

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

You have the operations in words. Which of them are plain create / read / update / delete, which are something else, and what goes wrong when a domain action is forced into a CRUD shape?

The situation

A scaffolding tool offers to generate a full CRUD for "Cart" — create, list, get, update, delete — with routes and a form. It looks like most of the work. But "add item" is not quite an update, "check out" is not any of the four, and "reserve stock" seems to be an update to a different table. You are not sure whether to bend the operations to fit or to leave the generator alone.

The reflex

Take the CRUD and bend the rest. Add item becomes "update cart with a new items array"; checkout becomes "update cart with status = ordered"; reserve stock becomes "update inventory with quantity − 1". Everything is an update on something, and the generated routes stay uniform.

Why it stalls

The rules disappear into the client. "Update cart with a new items array" lets the browser send two rows for the same product, a quantity of −1 or a product that does not exist; the find-before-append that add item was about lives nowhere, because an update has no algorithm — it replaces.

What the reflex produces — and fails to produce
  • The rules disappear into the client. "Update cart with a new items array" lets the browser send two rows for the same product, a quantity of −1 or a product that does not exist; the find-before-append that add item was about lives nowhere, because an update has no algorithm — it replaces.
  • "Update inventory with quantity − 1" is a lost-update bug with a route. Two customers reading 1, both writing 0, both reserving the last unit — the domain action "reserve" had a condition ("if available") that the CRUD verb erased.
  • "Update cart with status = ordered" pretends checkout is a field change. It is the creation of an order, the snapshot of prices, a payment and an inventory reservation; a status flip does none of these and cannot say what happens if one fails.
  • The generator produced routes, forms and a folder — motion — and the cart's behaviour was reduced to whatever the client chooses to PUT.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Sort each operation honestly. CREATE makes a thing that did not exist; READ returns state without changing it; UPDATE changes fields of an existing thing; DELETE removes it. Anything with a condition, an algorithm, a side effect on another concept or a result that is not just "the new state" is a DOMAIN ACTION, and the sort is not a formality: it decides where the rules live.
  • Recognise the tell of a mangled domain action: the client has to know the rule. If a correct request requires the browser to compute the new items array, the new stock count or the new status, the operation was a domain action wearing an update.
  • Keep CRUD where it is honest. Viewing a cart is a read; clearing it is a delete of its items; changing a quantity is an update with two rules attached — and the rules are cheap to enforce on an update whose input is one number. Not everything needs a verb of its own.
  • Where a domain action exists, give it its own name and its own contract: "reserve stock" takes a product and a quantity and either reserves or refuses; "check out" takes a cart and produces an order or a reason. The contract carries the condition that CRUD would drop (Operation Contracts).

The cart, sorted

The record's six operations placed in the five columns, with what makes each one sit where it does and what would be lost one column to the left. Read the last column: it is where the sort earns its keep.

OperationKindWhy that columnLost if treated as a plainer verb
Add itemUPDATE (with rules)Changes items of an existing cart; input is one product and a count.As a PUT of the items array: the find-before-append and the catalog check move to the client.
Remove itemDELETERemoves one entry.Nothing — it is honestly a delete; the no-op-on-absent choice is a contract detail.
Change quantityUPDATE (with rules)Sets one field of one entry.As a raw field write: negative values accepted; the zero-removes rule lost.
View itemsREADReturns state unchanged.Nothing.
Calculate totalDOMAINReads the catalog — another concept — and returns a computed value, not state.As a stored field read: the derived total becomes a stored one that drifts.
Clear cartDELETERemoves all entries.Nothing.
Reserve stockDOMAIN (inventory's)Condition on available quantity; a side effect in another concept.As "update inventory −1": the condition erased; two shoppers get the last unit.
Check outDOMAIN (orders')Creates an order, snapshots prices, calls payment, reserves stock.As "update cart status": none of the four things happen, and nothing can fail.

Deciding what an operation is

The decision is per operation and the options are the columns. What matters is the cost column — where the rule lives — because that is the thing CRUD generators quietly move to the client.

Where does this operation belong?

An operation is on the list. Which kind is it, and where do its rules live?

READ

when It returns state and changes nothing — view items.

cost None on the cart; staleness is the caller's problem.

CREATE / DELETE

when A thing begins or ends to exist — a cart, an entry, all entries.

cost Identity and ownership checks; what to do when the thing is already absent.

UPDATE with rules

when Fields of an existing thing change, and the rules can be checked from the input and the thing itself — change quantity.

cost Validation on the update, which the next developer can forget; small inputs keep it honest.

DOMAIN ACTION

when There is a condition on another concept's state, a side effect outside this concept, or an output that is not the new state — total, reserve, check out.

cost A named operation with its own contract, on the concept that owns the condition — and often a race to think about (Reasoning About Races: A Method, Not an Instinct in the concurrency domain).

What a mangled action looks like in production

Each row is a domain action that was shipped as a CRUD verb. The symptoms are ordinary bugs; the cause in every row is the same: a condition or an algorithm had no home.

Domain actions forced into CRUD
TriggerSymptomCauseResponse
Add item shipped as PUT /cart with the whole items arrayCarts with [Laptop × 1, Laptop × 1]; a cart containing a product that was never in the catalog.The find-before-append and the catalog check were left to the browser.POST /cart/items with { productId, quantity }; the server runs addItem.
Reserve stock shipped as read-then-update from the clientTwo orders for the last unit; negative stock.The condition "if available" was erased into a subtraction the client computed.A reserve action on inventory that checks and decrements in one step — the database's job at V3.
Checkout shipped as PATCH /cart { status: "ordered" }Orders with today's prices instead of the prices at purchase; "ordered" carts with no payment.A four-step action modelled as one field.POST /checkout producing an order, with every step's failure named.
Total shipped as a stored column updated by the clientTotals that disagree with the items.A derived domain read stored as a field.Compute it on the server in the response; drop the column.

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.

  • Make five columns and place every operation. Write beside each what would be lost if it moved one column to the left — usually a rule, a condition or a side effect.
  • For every UPDATE, ask whether the client could send a wrong new value. If the answer is "yes and the server would accept it", the operation either needs validation on the update or is a domain action in disguise.
  • For every candidate domain action, name the condition. Reserve stock: "if available". Check out: "if every item is still purchasable and payment succeeds". An action with no condition and no side effect was probably CRUD after all.
  • Model the honest domain actions as their own operations on the owning concept, not as updates on the cart. Reserve belongs to inventory; checkout belongs to orders. The cart is their input (What Can Happen to It?).
  • Run the What Can Happen to It? lab's sort for the cart and then for inventory; the warning it raises — "not every domain is CRUD" — is the point of the second one.

Worked on a concrete problem

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

  • The record sorts the cart by kind: add-item — update ("a new entry, or the existing entry's quantity"); remove-item — delete; change-quantity — update; get-items — read; get-total — domain ("the sum of price × quantity over the entries" — it reads another concept, the catalog, and returns a computed value rather than state); clear — delete. Five of six are honest CRUD with rules; one is a domain action because it crosses a boundary.
  • Add item is an update that is nearly a domain action: it has an algorithm (find, then increase or append) and two rules. The record keeps it as an update on the cart because its input is small and its rules are the cart's own; the API exposes it as POST /cart/items — "the item is the resource being created within the cart" — rather than as a PUT of the whole items array, precisely so the browser never computes the new array.
  • Beyond the cart: "reserve stock" is inventory's domain action, with the condition "quantity ≤ available" and the race the record names — "enforcing it in the cart alone is a race the inventory service has to resolve at checkout." "Check out" is orders' domain action: it snapshots prices, creates the order and calls payment. Neither is an update; both would lose their condition as one.

How you know it worked

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

  • Every operation is in one of five columns, and for each UPDATE you can say what the server validates.
  • No correct request requires the client to know a rule.
  • The domain actions have names, owning concepts and conditions, and none of them is a status field being flipped.
  • The generator's CRUD is used for the parts that were honestly CRUD, and left alone for the rest.

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 column does this operation belong to — and what would be lost if it moved left?
  • ?Could a client send this UPDATE a wrong value the server would accept?
  • ?What is the condition on this action, and which concept owns the state the condition reads?
  • ?Which of my concepts are honestly CRUD, and which have I been forcing?

What can go wrong

How the move itself fails
  • Everything becomes a domain action. setQuantity is renamed AdjustCartLineQuantityCommand, gets a handler and a bus, and the two-rule update it was is buried under a pattern. The sort exists to find the few operations that need more, not to promote all of them.
  • CRUD is rejected wholesale because "not every domain is CRUD" was heard as "no domain is". A product catalog's admin is CRUD, and building it as commands is ceremony.
  • A domain action is found and then implemented as a sequence of CRUD calls from the client — read stock, update stock — which is the lost update with extra steps.
  • The sort is done for the cart, where almost everything is CRUD, and never for inventory or orders, where almost nothing is.
What the move costs
  • A named domain action is a bespoke endpoint, a bespoke contract and a bespoke test, where a generated update was free; the freedom was paid for by moving the rule to the client.
  • Keeping add-item as an update-with-rules rather than a command keeps the code small and puts the rules in a validation step that is easy to skip when the next update is added.
  • Sorting is a judgement, and the profession draws the CRUD / action line differently; a team that disagrees about where add-item sits will argue about it more than once.
Misreads
  • "CRUD is for beginners; real systems use commands." Real systems use CRUD where it is honest and commands where a condition or a side effect needs a home; the record's cart is five CRUD operations with rules and one domain action.
  • "A domain action is anything with business logic." Change quantity has business logic (reject negatives, zero removes) and is still an update; the distinguishing marks are a condition on another concept's state, a side effect, or a result that is not the new state.
  • "REST means everything must be CRUD." REST means resources; POST /cart/items and POST /checkout are resource operations that carry an action. The API domain's Resource or Action? is the long version.

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.

  • GENERALThe five-column sort applies to any concept; the tell — "the client would have to know the rule" — finds mangled actions in chat (send is not "create message" when it has delivery rules), uploads and bookings just as it does in inventory.
  • CONTESTEDThe CRUD-first position, at its strongest: most application state really is four verbs on a table, rules belong in validation on those verbs, and separating "commands" from updates creates two code paths, two vocabularies and a bus for operations that differ only in a condition — which validation on an update could have carried. Practitioners holding this view build carts, catalogs and admin panels faster and only introduce named actions when a side effect genuinely spans concepts.
  • ILLUSTRATIVEThe scaffolding tool, the status = ordered flip and the two customers racing for one last unit are invented; the operation kinds are the shopping-cart record's.

Where the depth lives

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