Implementing a Product Configurator on a Website
A product configurator is an interface where a customer assembles the final version of a product from a set of parameters: material, size, color, components, engraving. Unlike simple variant selection (S/M/L), a configurator works with combinations: the number of variants can reach millions, and storing each as a separate SKU is impossible. The final price and composition are calculated dynamically.
Architectural choice: SKU vs dynamic configuration
Tabular variants (SKU) are suitable when there are up to thousands of combinations, and each has its own stock quantity. Standard approach for clothing.
Dynamic configuration is needed when:
- Number of possible combinations > 10,000
- Each parameter independently affects the price
- Made-to-order production (no pre-made inventory)
- Real-time result visualization is needed
Configurator data schema
CREATE TABLE configurator_products (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
name VARCHAR(255) NOT NULL,
base_price NUMERIC(12,2) NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE configurator_option_groups (
id BIGSERIAL PRIMARY KEY,
configurator_id BIGINT NOT NULL REFERENCES configurator_products(id),
name VARCHAR(255) NOT NULL, -- "Material", "Color", "Engraving"
type VARCHAR(50) NOT NULL,
-- select | radio | checkbox | text | number | color
is_required BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE configurator_options (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES configurator_option_groups(id),
label VARCHAR(255) NOT NULL,
value VARCHAR(255) NOT NULL,
price_modifier NUMERIC(12,2) NOT NULL DEFAULT 0, -- absolute surcharge
price_modifier_pct NUMERIC(5,2) NOT NULL DEFAULT 0, -- percentage surcharge
modifier_type VARCHAR(20) NOT NULL DEFAULT 'add', -- add | multiply | fixed
sku_suffix VARCHAR(50), -- for SKU formation
is_available BOOLEAN NOT NULL DEFAULT true,
image_url VARCHAR(500), -- option preview
sort_order INTEGER NOT NULL DEFAULT 0
);
-- Dependency rules between options
CREATE TABLE configurator_rules (
id BIGSERIAL PRIMARY KEY,
configurator_id BIGINT NOT NULL REFERENCES configurator_products(id),
rule_type VARCHAR(50) NOT NULL, -- requires | excludes | enables
if_option_id BIGINT NOT NULL REFERENCES configurator_options(id),
then_option_id BIGINT NOT NULL REFERENCES configurator_options(id)
);
Price calculation
The configuration price is calculated on the server, not on the client:
class ConfiguratorPriceCalculator
{
public function calculate(
ConfiguratorProduct $configurator,
array $selectedOptions // [group_id => option_id | value]
): PriceBreakdown
{
$base = $configurator->base_price;
$breakdown = [['label' => 'Base price', 'amount' => $base]];
$total = $base;
foreach ($selectedOptions as $groupId => $selection) {
$group = $configurator->optionGroups->find($groupId);
if ($group->type === 'text' || $group->type === 'number') {
// For free input — fixed surcharge per group
$modifier = $group->text_price_modifier ?? 0;
} else {
$option = ConfiguratorOption::findOrFail($selection);
$modifier = $this->resolveModifier($option, $total);
}
if ($modifier != 0) {
$breakdown[] = [
'label' => $group->name . ($group->type !== 'text' ? ': ' . $option->label : ''),
'amount' => $modifier,
];
$total += $modifier;
}
}
return new PriceBreakdown($total, $breakdown);
}
private function resolveModifier(ConfiguratorOption $option, float $currentTotal): float
{
return match($option->modifier_type) {
'add' => $option->price_modifier,
'multiply' => $currentTotal * ($option->price_modifier_pct / 100),
'fixed' => $option->price_modifier - $currentTotal,
};
}
}
The client shows the price preliminarily (from JS calculation), but when adding to the cart, the price is always recalculated on the server. You cannot trust client-side calculation in a financial context.
Dependency rules
Some options mutually exclude each other or require selecting another option. Rules are checked both on the client (UX) and on the server (validation):
class ConfiguratorRuleEngine
{
public function validate(ConfiguratorProduct $configurator, array $selected): ValidationResult
{
$errors = [];
foreach ($configurator->rules as $rule) {
$ifSelected = in_array($rule->if_option_id, $selected);
$thenSelected = in_array($rule->then_option_id, $selected);
match($rule->rule_type) {
'requires' => $ifSelected && !$thenSelected
? $errors[] = "Option requires selection: {$rule->thenOption->label}"
: null,
'excludes' => $ifSelected && $thenSelected
? $errors[] = "Incompatible options: {$rule->thenOption->label}"
: null,
'enables' => !$ifSelected
? $this->disableOption($rule->then_option_id)
: null,
};
}
return new ValidationResult(empty($errors), $errors);
}
}
Forming the configuration SKU
For production, it is important to have a deterministic configuration code:
class ConfigurationSkuBuilder
{
public function build(ConfiguratorProduct $configurator, array $selected): string
{
$parts = [$configurator->product->sku];
$sortedOptions = collect($selected)
->sortKeys()
->map(fn($optionId, $groupId) =>
ConfiguratorOption::find($optionId)?->sku_suffix ?? $optionId
);
return implode('-', array_merge($parts, $sortedOptions->all()));
// Example: CHAIR-BLK-LEATH-ARMB-CUST
}
}
API for frontend
The configurator works through AJAX. Minimal set of endpoints:
GET /api/configurators/{id} — full configurator schema
POST /api/configurators/{id}/price — recalculate price by selection
POST /api/configurators/{id}/validate — validate set of options
POST /api/cart/add-configuration — add configuration to cart
Price recalculation request:
POST /api/configurators/5/price
{
"selected": {
"1": 12,
"2": 7,
"3": "Ivan Ivanov"
}
}
Response:
{
"total": 4850.00,
"breakdown": [
{ "label": "Base price", "amount": 3500.00 },
{ "label": "Material: Natural leather", "amount": 1200.00 },
{ "label": "Color: Black", "amount": 0 },
{ "label": "Engraving", "amount": 150.00 }
]
}
Saving configuration in cart
The configuration is saved as JSON in a cart item:
ALTER TABLE cart_items ADD COLUMN configuration JSONB;
ALTER TABLE order_items ADD COLUMN configuration JSONB;
ALTER TABLE order_items ADD COLUMN configuration_sku VARCHAR(255);
{
"configurator_id": 5,
"groups": {
"1": { "option_id": 12, "label": "Natural leather" },
"2": { "option_id": 7, "label": "Black" },
"3": { "type": "text", "value": "Ivan Ivanov" }
},
"sku": "CHAIR-LEATH-BLK-CUST",
"price_at_add": 4850.00
}
When adding a configuration to the cart, the price is recalculated on the server and fixed. Price changes do not affect already added configurations.
Administrative interface
The manager should have the ability to:
- Create and edit configurators without a programmer
- Manage the order of groups and options
- Set dependency rules through UI (not SQL)
- Preview the configurator before publishing
Implementation timeline
- Basic schema + price recalculation API + add to cart: 5–7 days
- Dependency rules (requires/excludes): 2–3 days
- Administrative interface for configurator management: 3–4 days
- Visual preview (image change when option is selected): 2–3 days
- Integration with production task system: +3–5 days
Typical project: 2–4 weeks depending on the complexity of rules and visualization requirements.







