How to Build a Custom 'Product Configurator' Module for Complex Products in Magento 2

Hey — if you've ever had to build a product configurator in Magento 2 for complex items (think made-to-measure furniture, modular high‑tech devices, or custom clothing), you know it's a mix of data modelling, frontend UX, price/quote integration and performance work. In this post I’ll walk you through how to build a custom "Product Configurator" module in Magento 2 step by step. The tone is relaxed — like I’m explaining to a colleague — and I’ll include concrete code snippets you can copy, adapt and run.
What we’re building
A small but solid Magento 2 module that:
- Stores configurable options and attributes for complex products
- Provides a clean AJAX/JS frontend to choose options, preview choices and calculate price
- Integrates with the cart so configured products are added with correct price and options
- Is mindful of performance (caching, lazy loading options, indexes)
High-level architecture
Before diving into code, let’s agree on the architecture. For complex configurators I recommend:
- Keep product base data in standard Magento product model (catalog_product_entity).
- Store configuration definitions in custom tables (configurator_definition, configurator_option, configurator_option_values). This avoids overloading product attribute system for very complex option trees.
- Use product-level mapping (configurator_product_map) to tie definitions to products or product types.
- Expose a lightweight JSON API endpoint (Controller) that returns option trees and computed prices.
- Use client-side JS for the UI: fetch options, render interactive UI, call price calculation endpoint and add to cart via standard Magento add-to-cart but with extra options attached.
- Persist chosen configuration as quote item option and optionally as a separate product (if inventory or unique SKU needed).
Why custom tables?
Magento product attributes work for straightforward options. For configurable or hierarchical configurators (multi-level materials, dimensions, modular parts) a dedicated schema is more flexible — you can store dependencies, conditional rules and fast lookups.
Module skeleton
Create module Magefine_Configurator (you can use your vendor). Minimal file list:
- app/code/Magefine/Configurator/registration.php
- app/code/Magefine/Configurator/etc/module.xml
- app/code/Magefine/Configurator/etc/db_schema.xml (or Setup Patch for data)
- app/code/Magefine/Configurator/etc/frontend/routes.xml
- app/code/Magefine/Configurator/Controller/Option/Tree.php (JSON endpoint)
- app/code/Magefine/Configurator/view/frontend/web/js/configurator.js
- app/code/Magefine/Configurator/view/frontend/templates/product/configurator.phtml
- app/code/Magefine/Configurator/Observer/QuoteItemPriceObserver.php (adjust price)
registration.php
\?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_Configurator',
__DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magefine_Configurator" setup_version="1.0.0"/>
</config>
Database schema (concept)
Use db_schema.xml or a Patch to create three simple tables:
- magefine_configurator_definition: id, code, title, type (furniture/hightech/garment), json_schema (optional), created_at
- magefine_configurator_option: id, definition_id, parent_option_id (nullable), code, label, position, extra (json for metadata)
- magefine_configurator_option_value: id, option_id, sku_part (optional), value, price_adjust (decimal), image, extra (json)
Example db_schema.xml snippet (simplified):
<table name="magefine_configurator_definition" resource="default" engine="innodb" comment="Configurator definitions">
<column xsi:type="int" name="id" nullable="false" unsigned="true" identity="true"/>
<column xsi:type="varchar" name="code" nullable="false" length="255"/>
<column xsi:type="text" name="json_schema" nullable="true"/>
<constraint referenceId="PRIMARY" type="primary">
<column name="id"/>
</constraint>
</table>
Service layer
Create a repository or service class that reads the tables and builds a JSON-friendly option tree. Keep logic out of controllers. Example service skeleton:
class ConfiguratorService
{
private $definitionFactory;
private $optionFactory;
public function __construct(
\Magefine\Configurator\Model\DefinitionFactory $definitionFactory,
\Magefine\Configurator\Model\OptionFactory $optionFactory
) {
$this->definitionFactory = $definitionFactory;
$this->optionFactory = $optionFactory;
}
public function getOptionTreeForProduct($productId) {
// map product to definition -> fetch options -> build tree -> return array
}
}
Frontend: interface and JavaScript
Goal: create a fluid experience. Use a small JS widget to load the option tree via AJAX, render UI (radio, selects, color swatches), handle dependencies and compute price by calling the price endpoint. Keep most logic on client so interactions are snappy.
routes.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="magefine_configurator" frontName="configurator">
<module name="Magefine_Configurator" />
</route>
</router>
</config>
Controller: Option/Tree.php (returns JSON)
namespace Magefine\Configurator\Controller\Option;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
class Tree extends Action
{
private $jsonFactory;
private $configService;
public function __construct(Context $context, JsonFactory $jsonFactory, \Magefine\Configurator\Model\Service\ConfiguratorService $configService)
{
parent::__construct($context);
$this->jsonFactory = $jsonFactory;
$this->configService = $configService;
}
public function execute()
{
$result = $this->jsonFactory->create();
$productId = $this->getRequest()->getParam('product_id');
$tree = $this->configService->getOptionTreeForProduct($productId);
return $result->setData(['success' => true, 'data' => $tree]);
}
}
Frontend template (configurator.phtml)
<div id="magefine-configurator" data-product-id="<?= $block->getProduct()->getId()?>">
<!-- JS will render options here -->
</div>
<script type="text/x-magento-init">
{
"#magefine-configurator": {
"Magefine_Configurator/js/configurator": {}
}
}
</script>
JS: view/frontend/web/js/configurator.js
High-level responsibilities:
- Fetch option tree
- Render controls and bind change handlers
- Call price endpoint and update UI instantly
- Prepare payload for add-to-cart (including serialized choices)
define(['jquery'], function($){
'use strict';
return function(config, element) {
var $root = $(element);
var productId = $root.data('product-id');
var treeUrl = '/configurator/option/tree';
var priceUrl = '/configurator/price/calc';
function fetchTree() {
return $.getJSON(treeUrl, {product_id: productId});
}
function renderTree(tree){
// simple renderer: iterate options -> create elements
var html = '';
tree.forEach(function(opt){
html += '';
html += '';
html += '';
});
$root.html(html);
}
function collectChoices(){
var payload = [];
$root.find('.mf-option').each(function(){
var optId = $(this).data('id');
var valId = $(this).find('select').val();
payload.push({option_id: optId, value_id: valId});
});
return payload;
}
function updatePrice(){
var choices = collectChoices();
$.post(priceUrl, {product_id: productId, choices: JSON.stringify(choices)}, function(res){
if(res.success){
$('#price-box').text(res.price_formated);
}
}, 'json');
}
fetchTree().done(function(res){
if(res.success){
renderTree(res.data);
$root.on('change', '.mf-select', updatePrice);
updatePrice();
}
});
// Example: hooking into Add to Cart button
$(document).on('click', '#product-addtocart-button', function(e){
var choices = collectChoices();
// add hidden input with JSON choices and let Magento add-to-cart proceed
var $form = $('#product_addtocart_form');
$form.find('input[name="configurator_choices"]').remove();
$form.append('');
// let the default add-to-cart flow continue
});
};
});
Backend: price calculation and cart integration
Two ways to persist configured product price/metadata:
- Attach choice data and extra price to the quote item as an option (simpler)
- Create a separate virtual product/SKU per configuration (complex but better for inventory & analytics)
I’ll show the simpler approach: attach options and adjust the quote item price via an observer.
Controller: Price calculation (configurator/price/calc)
public function execute(){
$result = $this->jsonFactory->create();
$productId = $this->getRequest()->getParam('product_id');
$choices = json_decode($this->getRequest()->getParam('choices'), true);
$basePrice = $this->productRepository->getById($productId)->getPrice();
$extra = 0;
foreach($choices as $c){
$val = $this->optionValueRepo->getById($c['value_id']);
$extra += (float)$val->getPriceAdjust();
}
$final = $basePrice + $extra;
return $result->setData(['success'=>true, 'price' => $final, 'price_formated' => $this->priceHelper->currency($final, true, false)]);
}
Observer: adjust quote item price on add to cart
Listen to sales_quote_product_add_after or checkout_cart_product_add_after. Grab the configurator_choices from request and attach it to the quote item with custom option and set custom price.
class AddConfiguratorToQuoteObserver implements \Magento\Framework\Event\ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer)
{
$quoteItem = $observer->getEvent()->getQuoteItem();
$request = $observer->getEvent()->getRequest();
$raw = $request->getParam('configurator_choices');
if(!$raw) return;
$choices = json_decode(urldecode($raw), true);
// compute extra price like in price endpoint
$extra = 0;
foreach($choices as $c){
$val = $this->optionValueRepo->getById($c['value_id']);
$extra += (float)$val->getPriceAdjust();
}
$base = $quoteItem->getProduct()->getPrice();
$customPrice = $base + $extra;
// set custom price and add option
$quoteItem->setCustomPrice($customPrice);
$quoteItem->setOriginalCustomPrice($customPrice);
$quoteItem->getProduct()->setIsSuperMode(true);
$quoteItem->addOption(new \Magento\Quote\Model\Quote\Item\Option([
'product_id' => $quoteItem->getProduct()->getId(),
'code' => 'configurator_data',
'value' => json_encode($choices)
]));
}
}
Why set custom price like this?
Magento calculates price based on product price. Setting setCustomPrice and originalCustomPrice forces the quote item to use your computed price. It’s straightforward and works well for many stores; just be aware of tax calculation implications — test thoroughly with your tax settings.
Pricing rules and promotions
Some merchants want discounts or cart rules based on configuration choices (e.g. 10% off when a specific material is selected). Magento's Catalog and Cart Price Rules are powerful, but they don't natively inspect custom configurator JSON attached to quote items.
Options:
- Create dedicated SKUs for common configurations so Catalog/Cart Price Rules can use product SKU or attributes (recommended for frequently-used bundles).
- Extend Magento rule condition model to add custom conditions that read the quote item option 'configurator_data' and evaluate it. This requires implementing a custom Rule Condition class and registering it with salesRule condition combination.
- Apply discounts programmatically: create an observer on salesrule_validator_process or during quote totals collection to detect configurator selections and apply custom discount as a custom total.
Example: a simple observer that applies a small promo if an option with code 'premium_material' is selected:
// in collectTotals observer
foreach($quote->getAllItems() as $item){
$opt = $item->getOptionByCode('configurator_data');
if($opt){
$data = json_decode($opt->getValue(), true);
foreach($data as $d){
$val = $this->optionValueRepo->getById($d['value_id']);
if($val->getCode() === 'premium_material'){
$discount += $item->getRowTotal() * 0.1; // 10%
}
}
}
}
Performance optimization
Configurators can become slow if options are numerous (thousands of values) or if you compute expensive pricing rules live. Tips:
- Cache the option tree response with TTL for anonymous users. Use Varnish/Redis for caching JSON responses or a module-level cache keyed by product id and user scope.
- Lazy-load dependent option branches. Don’t return thousands of values in initial tree — fetch on demand when a parent option is selected.
- Precompute price matrices for common combinations and store them in a lookup table. The JS can query a fast price endpoint with chosen ids to return a price string — avoids heavy computation per request.
- Use database indexes on option_value lookup columns. Properly index definition_id, option_id and any frequently queried columns.
- Minimize server-side PHP during interactive flows. Offload price preview to client-side simple math when possible (if business rules allow), and validate server-side only on add-to-cart.
- Compress JS/CSS and bundle assets. Magento’s static content deployment and built-in bundling will help, or use your own front-end build system.
Concrete use cases and modeling examples
1) Custom furniture (made-to-measure)
Requirements: dimensions (numeric), material (wood/metal), finishes (multiple), upholstery fabric, lead time impacts price.
Modeling tips:
- Store dimensions as numeric inputs in the configurator option values but validate ranges in PHP (or with JSON schema in the definition).
- Keep materials as option_value rows with price_adjust and inventory flags (if you need to reserve material stock).
- Lead time: if certain combinations increase lead time, store a lead_time field in option_value and surface to customer in UI.
Example option value for wood:
{
"id": 423,
"option_id": 12,
"value": "Oak",
"price_adjust": 150.00,
"extra": {"lead_time_days": 14, "sku_part": "WOOD-OAK"}
}
2) High‑tech configurable device
Example: modular laptop where RAM, CPU, GPU, storage and accessories combine. Some components are incompatible — you need conditional rules.
Implementation tips:
- Store compatibility matrix in extra JSON on option_value or in a separate table for faster joins.
- JS should hide/disable incompatible choices when the user selects a dependent option.
- Consider rendering a live specification summary and SKU snippet so downstream systems (ERP) can parse the final config if needed.
3) Customized clothing
Example: custom jacket — size, fabric, lining, monogram. Sizes may map to measurements and affect pricing.
Implementation tips:
- Allow numeric or select-based sizing and map to fit types (regular/slim/plus) in server logic.
- Monogram: treat as textarea or structured input and store as part of configurator_data so production can consume it.
- Offer visual swatches and allow uploads (e.g. upload an image for embroidery) — be careful with file upload security and storage.
UX tips for a smooth configurator
- Show a sticky summary area with price preview, selected options, and validation messages.
- Make changes instant with AJAX but fall back to full server validation on add-to-cart.
- Use images and swatches for choices — it dramatically improves conversion.
- Provide clear messages for outdated caches or unavailable combos (e.g. "This finish is not available with 240cm width").
- Test on mobile: touch-friendly controls and a condensed summary panel are essential.
Testing and QA
Checklist:
- Unit tests for price calculation service and compatibility rules
- Integration tests for controller endpoints and observer behavior
- Manual QA covering edge cases: max/min dimensions, incompatible choices, tax and shipping impacts
- Performance load test on price endpoint and option tree endpoint — emulate many users
Security considerations
Don’t trust client data for price or inventory decisions. Always validate choices server-side before finalizing the quote and again during order placement. Sanitize/validate any free-form text (monogram) and safely handle uploads (scan, store outside webroot or use Magento media storage).
SEO and content for Magefine
When writing the product pages that use this configurator on magefine.com, include clear feature sections like "Customization options", "Lead time & production", and "Pricing examples" so search engines can surface the value of configurable products. Since Magefine provides Magento 2 extensions and hosting, highlight integrations (e.g. how configurator works with Magento cart, promotions, and performance tips like using Redis).
Advanced ideas
- Use a headless approach: serve configurator UI from a separate SPA that talks to Magento via REST APIs.
- Create a visual builder for merchant admins — allow non-developers to define option trees and price rules using JSON schema or a UI.
- Integrate with external CPQ (Configure-Price-Quote) systems if your pricing rules are extremely complex or require approvals.
Wrap up
Building a robust Product Configurator in Magento 2 is a rewarding challenge. The key takeaways:
- Prefer a dedicated data model for complex option trees.
- Keep the frontend snappy with JS/AJAX and lazy-loading of heavy data.
- Integrate cleanly with cart using quote item options and custom price; consider dedicated SKUs for heavily promoted configs.
- Plan for performance: caching, indexing, and precomputed matrices.
If you want, I can share a small sample module repository with the skeleton code above (db schema, controller, a basic JS widget and observer). Also, if you need help adapting any of the examples to a real product (furniture/hightech/clothing), tell me which use case and I’ll tailor the code.
Good luck and let me know how it goes — happy to review your module and help optimize it for magefine.com hosting and performance.