An e-commerce schema in MongoDB is a series of small modeling calls, and most of them answer to one question: is this data read with its parent, and is it bounded? Embed when yes, reference when no. There is one rule specific to commerce that trips people up, and we will get to it, because it is the one that turns into a bug six months in. Here is a concrete model you can adapt.
Products: embed the variants
A product is read as a unit, and its variants (sizes, colors) are bounded, so embed them:
// products
{
_id: ObjectId("..."),
slug: "arctic-hoodie",
name: "Arctic Hoodie",
price: 6900, // cents
variants: [
{ sku: "AH-S-BLU", size: "S", color: "blue", stock: 12 },
{ sku: "AH-M-BLU", size: "M", color: "blue", stock: 4 }
],
ratingAvg: 4.6, // summary of reviews, kept here for the listing
ratingCount: 218
}
One read renders the product page. The ratingAvg and ratingCount are a deliberate copy of a summary you always show, so the listing does not have to touch the reviews collection.
Cart: embed the items, look up the price live
A cart is small, bounded, and always read with its owner. Embed it, store the productId and quantity, and resolve the current price when you display it:
// users (or a carts collection keyed by user)
{
_id: 123,
cart: [
{ productId: ObjectId("..."), sku: "AH-M-BLU", qty: 1 },
{ productId: ObjectId("..."), sku: "MUG-01", qty: 2 }
]
}
The cart does not store price, because a cart should reflect the price now. You look that up from the product at display time. The moment money changes hands, that changes.
Orders: snapshot everything (this is the one)
Here is the rule that is specific to commerce. An order is a historical record, and prices and products change. If your order references the live product, then a price change next month silently rewrites what the customer paid, and a deleted product breaks the order entirely. That is the classic version of a schema decision coming back to bite: the reference was correct for the cart and wrong for the order.
So at checkout, copy the details into the order:
// orders
{
_id: ObjectId("..."),
userId: 123,
placedAt: ISODate("2026-07-01T14:12:00Z"),
status: "shipped",
items: [
{ productId: ObjectId("..."), name: "Arctic Hoodie",
sku: "AH-M-BLU", price: 6900, qty: 1 } // price AS OF purchase
],
total: 6900
}
The name and price are frozen at purchase time. The product can change or vanish and the order still tells the truth. This is intentional denormalization: the cart references, the order snapshots, and they are different documents for a reason. The general principle behind that call is in how to design a MongoDB schema.
Reviews: reference, do not embed
Reviews are unbounded. A popular product can gather thousands, and a document caps at 16MB, so embedding them is a wall you will hit. Give them their own collection:
// reviews
{ _id: ObjectId("..."), productId: ObjectId("..."), userId: 123, stars: 5, body: "..." }
Keep the summary (ratingAvg, ratingCount) on the product for the listing, and recompute it when a review lands. Embed the summary you always show, reference the full set you sometimes page through.
Then watch it drift
A good e-commerce schema still drifts under real traffic: an import writes price as a string, a new variant field appears on some products and not others, an order slips through without a total. Those are the mismatches that break a checkout query without throwing an error. Catching them is analyzing your schema, and enforcing the shape once it settles is $jsonSchema validation.
mongoui's Schema doctor samples each collection and reports the real field shapes and type drift, so you can confirm the products, carts, and orders still match the model you designed. It runs on your machine, so your catalog and customer data never leave your network. Embed the cart, snapshot the order, reference the reviews, and check now and then that reality still agrees with the diagram.
