What an Order Must Remember

Suppose the shopper buys two blue mugs on Sunday, while the weekend promotion is still running. Each mug costs 1800 cents. On Monday, the promotion ends and the product’s price returns to 2000 cents. When the shopper opens Sunday’s order, it must still show 1800 cents per mug and a total of 3600 cents.

A cart and an order hold different data. The cart shows what the shopper is thinking of buying, at current prices. The order records what the shop accepted at the time of purchase. We made this distinction when we wrote the shop API’s requirements. Now we use it to decide how to store the order.

Recording the purchase

Suppose an order item held only a product identifier and a quantity, just like a cart item. If we looked up the product’s current price when displaying the order, Sunday’s purchase would appear to have cost 4000 cents. That is because the product document holds only the current price.

So when we place an order, we copy each product’s name and purchase price into the order’s items. Each item also records the product identifier and the quantity purchased. Here is an order for the two mugs:

{
  "_id": "92",
  "shopId": "mugshop",
  "shopperId": "8",
  "cartId": "75",
  "items": [
    {
      "productId": "42",
      "productName": "Blue mug",
      "unitPriceCents": 1800,
      "quantity": 2
    }
  ],
  "totalCents": 3600,
  "placedAt": "2026-09-27T14:03:00Z"
}

Each item belongs to this order, and we read the items together when displaying it. Like a cart, an order contains only the items selected for one purchase, and we expect that list to be short. So we embed the items in the order document.

We copy only the details the order needs to keep, not the whole product. The product’s current stock, for example, is not part of the purchase record. The productId still links the item to the product in the catalog. But we do not use it to look up the name or price that the order shows.

The stored total is each item’s purchase price times its quantity, added up over the items. When we create the order, the total must match the embedded items. This version of Shopend does not handle payment processing, taxes, or shipping.

The copied name and price are a snapshot of the product at the time of purchase. They stay the same even if the merchant later renames or deletes the product. A copy of current data is different: we must update it whenever its source changes. We must not update a snapshot when its source changes. The same idea applies in a relational database. There, an order_items table would store the purchase details too.

Reading the order

With one lookup, the backend can get the purchase details and also check that the order belongs to the shopper. The lookup uses the order identifier, the shop identifier, and the authenticated shopper’s identifier:

db.orders.findOne({
  _id: "92",
  shopId: "mugshop",
  shopperId: "8"
})

The result has what the order page shows: the names, purchase prices, quantities, total, and time. We do not need to read any product documents to display those purchase details.

Creating this document is only part of placing an order. We must also reduce stock in the product documents and mark the cart as ordered. We will return to how these changes succeed or fail together.