How product versioning works
A worked example with real, normalized data (normalized — each fact stored once, in separate linked tables). The design workbench loads a product as a nested array (line → form → provision → options). That array is a design-time view; underneath, production stores it as three tables — provisions, shareable options, and a junction that links them with effective/expiry dates + version. This page shows those tables, then changes the product and shows exactly what is written.
0 · A product has two versioned parts
1 · The real shape — three tables (not one)
A provision has many options; an option is shareable — the same option record can hang off more than one provision (and more than one product). So provisions and options are separate tables, joined by a third.
| provision_id | name |
|---|---|
| PRV-COVA-LS | Coverage A — loss settlement |
| PRV-DED | All-perils deductible |
| PRV-WB | Water Backup sub-limit |
| PRV-JWL | Jewelry theft sub-limit |
| option_id | label |
|---|---|
| OPT-RC | Replacement Cost |
| OPT-ACV | Actual Cash Value |
| OPT-1000 | $1,000 |
| OPT-2500 | $2,500 |
| OPT-5K | $5,000 shared |
| OPT-10K | $10,000 |
| OPT-25K | $25,000 |
OPT-5K ("$5,000") is just a value — it isn't "owned" by Water Backup. It can be linked under Water Backup and Jewelry (below). That's why dates can't live on the option.
| product | provision_id | option_id | default? | effective | expiry | rate_id |
|---|---|---|---|---|---|---|
| HO3 | PRV-COVA-LS | OPT-RC | default | 2026-01-01 | — | R-COVA-RC |
| HO3 | PRV-COVA-LS | OPT-ACV | 2026-01-01 | — | R-COVA-ACV | |
| HO3 | PRV-DED | OPT-1000 | default | 2026-01-01 | — | R-DED-1000 |
| HO3 | PRV-DED | OPT-2500 | 2026-01-01 | — | R-DED-2500 | |
| HO3 | PRV-WB | OPT-5K | default | 2026-01-01 | — | R-WB-5K |
| HO3 | PRV-WB | OPT-10K | 2026-01-01 | — | R-WB-10K | |
| HO3 | PRV-JWL | OPT-5K | default | 2026-01-01 | — | R-JWL-5K |
2 · Changing the product — add & expire links, never edit a value
On 2026-04-01 we want a $25,000 Water Backup choice. We do not change the $5,000 option to $25,000 (that would rewrite history for everyone who chose $5,000). Two valid ways:
| provision | option | effective | expiry |
|---|---|---|---|
| PRV-WB | OPT-5K | 2026-01-01 | — |
| PRV-WB | OPT-10K | 2026-01-01 | — |
| PRV-WB | OPT-25K insert | 2026-04-01 | — |
New business (newly sold policies) after Apr 1 sees three choices. One row inserted; nothing else touched. The $25k option may already exist (shared) — you just link it.
| provision | option | effective | expiry |
|---|---|---|---|
| PRV-WB | OPT-5K | 2026-01-01 | 2026-03-31 set expiry |
| PRV-WB | OPT-10K | 2026-01-01 | — |
| PRV-WB | OPT-25K insert | 2026-04-01 | — |
New business after Apr 1 sees $10k & $25k. We set an expiry on the junction row — we do not delete it or edit OPT-5K.
3 · So what is "V1 / V2"? — a date window in its own table
Effective dates are the truth. A version label (V1, V2) is a row in a separate product_version table that names an effective window — used for filing/communication. You don't duplicate anything to make V2; you point a label at a date.
4 · How a product loads — the join that builds the nested array
The workbench's nested line → provision → options array is the result of one join, filtered by the as-of date (or the version's window):
5 · Bitemporal — "two clocks" (for backdated changes)
Bitemporal (by-temporal, "two times") = every junction row records two dates: when it's true in the real world (valid-time = effective/expiry) and when the system recorded it (transaction-time). You need both the moment you do anything backdated.
| junction row | valid from (real world) | valid to | txn from (we recorded) | txn to |
|---|---|---|---|---|
| PRV-WB · OPT-25K | 2026-04-15 | ∞ | 2026-04-15 | 2026-06-10 |
| PRV-WB · OPT-25K | 2026-04-01 | ∞ | 2026-06-10 | ∞ |
Row 1 = what we believed (available from Apr 15) until the Jun 10 correction. Row 2 = the corrected truth (available from Apr 1). Nothing is overwritten — both are kept, so you can answer "as-of any date, on either clock." A single "last_updated" timestamp can't do this.
6 · Rate — priced per option, on its own version
Each junction row points to a rate_id; the rate for an option lives in a separate, separately-versioned rate table. A rating method (R1 — the versioned pricing algorithm; it becomes R2 when the formula itself changes, not just the numbers) turns the customer's selected options into a premium — each option contributes its factor.
| rate_id | for option | rate effect |
|---|---|---|
| R-DED-1000 | All-perils ded $1,000 | base |
| R-WB-5K | Water Backup $5,000 | +$60 |
| R-WB-10K | Water Backup $10,000 | +$80 |
| R-WB-25K | Water Backup $25,000 | +$95 new w/ the option |
Adding the $25k option adds one rate row (R-WB-25K) — it doesn't change the price of $5k or $10k. That's the pattern in one line: the rate may vary, but we're providing a new option — pricing and product structure move independently.
| Step | Selected option | Factor | Running premium |
|---|---|---|---|
| Base rate | HO-3 · territory 12 (a geographic rating zone) · TIV $1.1M | — | $1,200 |
| Coverage A | Replacement Cost (default) | ×1.00 | $1,200 |
| Deductible | $2,500 (chosen) | ×0.91 | $1,092 |
| Water Backup | $25,000 (chosen) | +$95 | $1,187 |
| Catastrophe load | cat-model wildfire 78 | ×1.45 | $1,721 |
| Annual premium | frozen at bind w/ the picked option_ids + rate 2026.1 | $1,721 | |
7 · The authoring lifecycle — draft → effective → superseded
"Make the provision/option but don't put it into effect yet — experiment first" is a classic carrier authoring step: it's the draft state — junction rows not yet given an effective date.
Provisions and options are separate, normalized tables; options are shareable, so one option can hang off many provisions. The link between them — the product_provision_option junction — is where effective/expiry dates and the version live. To change a product you add a junction row (offer a new option) and/or set an expiry on one (retire an option); you never edit an option's value or delete a link, so history stays intact. A "version" (V1/V2) is just a labeled effective-date window in its own table — the offered option set is resolved by a dated join, not by cloning. Each junction row carries its own rate (separately versioned), and a bound policy pins the picked option_ids + rate version so it is reproducible forever.