3.0 KiB
ADR-0016: Option values are retained when variants reference them
- Status: Accepted
- Date: 2026-08-12
Context
A merchandiser removes the "Red" colourway from a product. What should happen to the three Red variants and to the Red option value itself?
Three things reference them and each has a different claim:
- Order history — an order line points at a variant id, and reading that order later needs "Red / M" to still mean something.
- The variant matrix — Red must stop generating combinations.
- The storefront — a shopper must not see a Red swatch that can never be selected.
The database already takes a position: ProductVariantOptionValue.optionValue
is onDelete: Restrict, precisely so that deleting a value cannot silently
orphan or cascade into history.
Decision
Variants are archived, never deleted. A removed combination sets
status = ARCHIVED. The row, its SKU and its option links all survive, so an
order placed last month still resolves.
Option values are deleted only when nothing references them. If any variant — active or archived — still links to a value, the value is retained. It stops appearing in the matrix and stops being offered, but it continues to exist so the archived variants remain readable.
The matrix is built from the operator's submitted option set, not from the database. Retained values would otherwise regenerate the very variants that were just archived.
The storefront filters option values to those offered by at least one active variant. Without this last step, a retained value renders as a permanently disabled swatch with no explanation.
Consequences
Order history stays intact and readable, which is the constraint that drove everything else. Re-adding a removed colourway later reuses the retained value and its translations rather than creating a duplicate.
The cost is that product_option_values accumulates rows that are invisible to
shoppers, and "delete" is not always literally a delete — a merchandiser who
inspects the database will find values they thought they removed. The schema
comment and this ADR are the mitigation; a future admin screen could surface
retained values explicitly.
This was found the hard way: the first implementation deleted values before
archiving variants and hit the Restrict constraint as a 500. The failure was
the schema doing its job.
Alternatives considered
Cascade the delete. Removes the option value and its variant links, which
either breaks order lines or cascades into them. Rejected — this is the outcome
Restrict exists to prevent.
Soft-delete option values with an isActive column. Functionally similar to
retention, but adds a column and a filter to every catalog query for a case that
is already handled by "is any active variant offering this?". Rejected as
redundant state.
Refuse to remove a value that has variants. Simple and safe, but forces the merchandiser to archive six variants by hand before they can drop a colourway — pushing bookkeeping onto the person the tool exists to help.