PRD: Product options & variants
| Module | Products | PRD ID | PRD-OPT-001 |
| Status | Ready to dev | FEAT | OPT |
| Epic | — | Plane | BANA-1553 |
| Date | 2026-06-12 | Version | v1.0 |
| Packages | @nx/commerce · @nx/core | URD | OPT |
| Surface | Client · Owner/MgrSale · POS | ||
| Owner | Phát Nguyễn | ||
What product options & variants are
Variants tell us a product has several sellable forms, but not why they differ. With free-text names alone, nothing stops two identical variants, the POS can't tell size apart from ice, and there's no stable key to match against an external listing.
This PRD makes an option, its option values, and each variant's combination explicit - declared by the owner, kept consistent by the system. Owners describe how a product varies - options like size, ice, or colour, each with its own list of values - and every sellable variant picks one value per option.
Two rules keep it honest
| Rule | Meaning |
|---|---|
| A value must belong to the product | A variant cannot use a value from an option owned by a different product |
| No two variants may share a combination | The same set of chosen values can only ever belong to one variant in the product |
Because of these two rules, the POS, online channels, and reporting can all treat a combination as the variant's identity - pull the combination and you know exactly which variant it is.
One product, from declaring options to picking an order
An owner sets up a milk tea with size (S/M/L) and ice (100/50/0), then declares the five combinations actually sold. A cashier then picks a variant by choosing size, then ice - not from one long list.
A second "Large, 50%" variant is refused because the combination already exists; choosing two sizes for one variant is also refused because a variant takes at most one value per option.
Where this is easy to get wrong
The system does not generate every combination for the owner
Most merchants don't sell the full matrix. Owners declare only the combinations they actually sell; the system never auto-explodes Size × Ice into every possibility and expects the owner to delete the rest. Declaring only what's sold keeps the catalogue equal to what's actually sellable - no phantom variants to hide, price, or stock.
1. Goals & Non-Goals
Goals
- Options per product, each an ordered list of values shown in the owner's order.
- Each variant takes one value per option it uses.
- The system guarantees every combination is valid and unique within the product.
- A merchant setting decides whether every variant must use options, or whether option-free variants are allowed.
Non-Goals
- No auto-generating the full matrix - owners declare only the variants they actually sell.
- No price or stock on a value - pricing lives in fares (FAR), stock in Inventory.
2. Success Metrics
| Metric | Target |
|---|---|
| Duplicate combinations | Zero - a repeated combination is always refused |
| Invalid combinations | Zero - a value not on the product is never accepted |
| Strict-options enforcement | With the setting on, no option-free variant can be created |
3. Personas & Use Cases
| Persona | Goal |
|---|---|
| Owner / Manager | Define how a product varies and the choices offered, in display order |
| Cashier | Pick a variant by choosing size, then ice - not from one long list |
| Channel integration | Match an external listing to the right variant by its combination |
Core scenario: an owner sets up a milk tea with size (S/M/L) and ice (100/50/0), then declares the five combinations actually sold. A second "Large, 50%" variant is refused; so is choosing two sizes for one variant.
4. User Stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | owner | define a product's options and values once | every variant just picks from them |
| 02 | owner | an existing combination refused | the catalogue never holds two entries for the same thing |
| 03 | owner | decide whether option-free variants are allowed | simple products stay simple and complex menus stay disciplined |
| 04 | cashier | see variants grouped by option | ordering is a few taps |
5. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | Owner can define a product's options. Each option is shown in a chosen order. | ✅ | URD-OPT-001 |
FR-002 | Each option carries an ordered list of values. | ✅ | URD-OPT-002 |
FR-003 | An option and its values are created or changed together in one step. | ✅ | URD-OPT-003 |
FR-004 | A variant declares its value on each option it uses. An unknown option or value is refused. | ✅ | URD-OPT-004 |
FR-005 | A variant takes at most one value per option. | ✅ | URD-OPT-005 |
FR-006 | Every value a variant uses must belong to the product and to its option. | ✅ | URD-OPT-006 |
FR-007 | No two variants of a product may share the same combination. Option-free variants are exempt. | ✅ | URD-OPT-007 |
FR-008 | A merchant setting can require every variant to use options. Option-free variants are refused. | 🔶 | URD-OPT-008 |
FR-009 | An owner can change which options a variant uses. | ✅ | URD-OPT-009 |
FR-010 | An option offered with no values is refused. A duplicated choice is refused. | 🔶 | URD-OPT-010 |
5.1 Acceptance Criteria
- Define a new option for a product together with its list of values in one call - the option and every value save together, shown in the declared order.
- Edit an existing option, adding one new value and removing one old value in the same call - both changes land together.
- Declare a variant that picks one value per option it uses - the variant saves, bound to exactly those values.
- Declare a variant that uses an option or value not on the product - refused.
- Declare two values for the same option on one variant - refused.
- Declare a second variant with the exact same value combination as an existing one - refused; changing any single value in the combination is accepted.
- Turn the strict-options setting on, then create a new product with a variant that picks no options - refused. With the same setting on, adding an option-free variant to an existing product - through the edit-product screen or through creating a standalone variant - is still accepted today; the setting isn't applied on those two paths yet.
- Edit an existing variant to switch to a different option - the new combination is checked against the product's options and against every sibling variant before it saves.
- Create a new option for a product with no values, or two values with the same name in the same option, while creating the product - refused. The same action through the standalone option-management screen (not through product creation) - an option with no values is still created today; a duplicate value name is still blocked.
6. Non-Functional Requirements
| Area | Requirement |
|---|---|
| All-or-nothing | An option and a variant's values save as one unit: all changes land, or none do |
| Per-merchant isolation | Options belong to one product under one merchant; never shared across products or merchants |
| Languages | Option and value names display in multiple languages |
| Order-independent | A combination is the same whatever order its values are listed |
7. UX & Flows
8. Data & Domain
| Concept | Role |
|---|---|
| Option | A way a product varies (size, ice level), with an ordered list of values |
| Option value | One choice on an option (S / M / L) |
| Combination | The values a variant takes - its identity within the product |
An option's key is unique within its product (uniqueness rides on the key, not the display name); a value is unique within its option; a combination is unique among the product's variants.
9. Dependencies & Assumptions
Depends on
| # | Feature | What it depends on |
|---|---|---|
| 01 | Variants (PRD-PRD-001) | options attach to variants, they don't replace them. |
Assumptions
| # | Assumption | If wrong |
|---|---|---|
| 01 | The strict-options setting is read when a variant is saved; turning it on later does not re-check variants that already exist. | If the system retroactively re-checked every existing variant whenever the setting changed, a merchant that once allowed option-free variants would suddenly have "violating" variants without anyone editing them - turning strict options on only blocks NEW changes, it never touches existing data. |
10. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 - see URD feature catalog |
| Rollout | Ships with the variant capability; all merchants, no feature flag |
| Migration | None - options and combinations are validation logic on the existing variant create/update path, with no legacy data to convert |
| Launch criteria | Invalid and duplicate combinations are refused with a clear reason; uniqueness holds even for variants created at the same instant; the POS shows options in the owner's order |
| Monitoring | Rate of duplicate or invalid combinations refused at variant creation |
References
- URD: OPT - Product Options · VAR - Variants
- Sibling PRDs: Product catalogue, variants & identifiers · Fare system & pricing
- Developer: @nx/commerce
Risks & Open Questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | Removing a value still used by a variant could break an existing variant | Governed by the merchant's deletion policy, not a free-for-all delete |
| 02 | Turning the strict-options setting on does not retroactively touch existing option-free variants, which could read as the setting not working | Only new changes are checked - existing variants are left untouched, and this is deliberate behavior |
Frequently Asked Questions
| # | Question | Answer |
|---|---|---|
| 01 | Why not generate every combination for me? | Most merchants don't sell the full matrix. Declaring only what's sold keeps the catalogue equal to what's actually sellable - no phantom variants to hide, price, or stock. |
| 02 | Can two products share options? | No - options belong to one product. Shared templates may come later if duplication starts to hurt. |
| 03 | Where do option-dependent prices go? | On the variant's fare. The combination identifies the variant; the fare prices it. |