Files
web_sport/docs/adr/0003-product-and-productvariant-as-separate-entities.md

1.8 KiB

ADR-0003: Product and ProductVariant as separate entities

  • Status: Accepted
  • Date: 2026-08-11

Context

A "Running Shirt" in black, size M is a different physical good from the same shirt in white, size L: different barcode, different stock, potentially different price. Modelling sizes: string[] and colors: string[] on a product makes every one of those facts unrepresentable.

Decision

Product is the marketing entity — it has a name, a slug and a page, and deliberately has no SKU, no price and no stock. It owns an ordered list of ProductOptions (Colour, Size), each owning ordered ProductOptionValues. Every purchasable combination is a ProductVariant with its own SKU, price, sale price, barcode, weight and stock.

ProductVariantOptionValue resolves a variant to exactly one value per option, with @@id([variantId, optionId]) enforcing at the database level that a variant cannot have two colours. Stock lives in StockLevel keyed by (variant, location), never on the variant row.

Consequences

Cart lines, order lines, stock movements and marketplace listings all reference a variant id — the same granularity Shopee, Lazada, TikTok Shop, ERP and POS systems use, so integrations map 1:1 instead of needing a translation layer. Adding a third option (width, fit) is data, not a migration.

The cost is real: the PDP must resolve option selections to a variant, and the admin needs a variant-matrix editor rather than two text inputs. That cost is paid once and is the reason the model survives contact with a warehouse.

Alternatives considered

Size/colour as columns on Product — rejected: cannot express per-combination stock or price, which is the entire job. A single flat SKU table with no product grouping — rejected: there would be nothing to hang a product page, gallery or description on.