Designing the Product Document

Putting the attributes next to the other fields causes a problem. The merchant chooses the attribute names, so a merchant could choose a name we already use. Suppose a candle shop sells cedar candles in packs of three, and the merchant adds an attribute called quantity to say how many candles are in a pack. The product’s quantity field holds the stock, and the merchant’s attribute would replace it. Our code would then read “3 candles” as the number of packs in stock.

To avoid this, we keep all of a product’s attributes in one field, attributes, whose value is an object:

{
  "_id": "9001",
  "shopId": "wickandwax",
  "name": "Cedar candle",
  "description": "Hand-poured soy candles with a cedar scent.",
  "priceCents": 3000,
  "quantity": 20,
  "attributes": {
    "scent": "cedar",
    "burn time": "40 hours",
    "quantity": "3 candles"
  }
}

Now the stock and the merchant’s quantity attribute are two different fields. From here on, every product keeps its attributes this way. That includes the blue mug and the linen shirt.

We define the fields outside attributes. They have the same names and meanings in every product, and our code depends on them. For example, our code reads priceCents to compute a cart’s total, and it reduces quantity when an order is placed. The merchant defines the fields inside attributes. We store them and return them to the storefront, but our code does not use them for anything else.

What the database checks

In a relational table, the columns are part of the schema, and a row can hold only those columns. The schema can also add constraints. For example, it can require that a price is not null or negative.

A MongoDB collection does not have a required set of fields, so it accepts a document of any shape. We can give the collection validation rules for the database to check. For example, we can require that every product has a priceCents field that holds a whole number of 0 or more. These rules check only the fields they name.

For Shopend, we give the products collection validation rules for the fields outside attributes, so the database checks the fields every product has. We give it no rules for the fields inside attributes, because the merchant defines those. We could not do this if the attributes were columns in a relational table, because every column has to be defined in the table’s schema.

Changing the shape later

In a relational table, every row has the columns in the table’s schema. After we add or remove a column, every row has the new set of columns.

A collection does not work this way. Suppose we had launched Shopend with the design from the previous section, where the attributes were next to the other fields, and moved them into attributes later. From then on, our code would write new products with an attributes field. The products already stored would still have their attributes next to the other fields. The database does not change them for us.

Validation rules do not change them either. MongoDB checks the rules when a document is inserted or updated. When we add or change a rule, it does not check the documents already stored.

So we have two options. We can run a script that rewrites every stored product in the new shape. Or we can write our code to read both shapes. Our code would then look for attributes inside attributes and also next to the other fields.