Variation Generation and Performance with Many Combinations
Configurable products are the standard tool in Magento 2 for representing size, color or material as independent, stock-managed variations. Anyone who understands the data model built from catalog_product_super_attribute and catalog_product_super_link, generates variations programmatically instead of manually in the admin grid, and designs indexers and Full Page Cache for large combination counts avoids the typical performance traps that slow down configurable catalogs with 10,000 or more SKUs.
Table of Contents
- 1. When configurable products are the right choice
- 2. Data model: catalog_product_super_attribute, catalog_product_super_link
- 3. Attribute set design: choosing configuring attributes correctly
- 4. Programmatic variation generation in detail
- 5. Performance with many combinations: indexer and EAV joins
- 6. Frontend performance: JSON config payload and swatches
- 7. Caching strategies: Full Page Cache per parent product
- 8. Stock and salable quantity per variation
- 9. Configurable products compared: Configurable vs. Bundle vs. Grouped
- 10. Summary
- 11. FAQ
1. When configurable products are the right choice
Configurable products are the standard way in Magento 2 to offer a product in several variations such as size and color, where each variation remains an independent simple product with its own SKU, its own stock level and its own price. The customer sees a single parent product on the product detail page, selects the desired combination of configuring attributes via dropdown or swatch, and Magento resolves this selection server-side to the matching child product. This mechanism differs fundamentally from the bundle product, where several independent products are assembled into a set, and from the grouped product, which merely displays a list of related but independently purchasable products on a shared page.
The central difference to bundle and grouped lies at the attribute level: bundle options are freely configurable selection groups without an EAV relation to the main product, whereas configurable products are controlled via real product attributes such as color or size, explicitly marked as configuring in the attribute set. This makes them ideal for cases where variations can be clearly described via one to three attributes and every combination should exist as an independent, stock-managed product, for example a T-shirt in five sizes and six colors.
Not every variation requirement should be solved via configurable products, however. As soon as more than three to four configuring attributes are combined, or most combinations are rarely actually ordered, for example with individually assembled furniture with material, fabric, handle and leg variation, the number of child products grows exponentially and data maintenance becomes impractical. In such cases, bundle products with dynamic price calculation or a custom-options concept of their own are often the better choice, because they do not require a physical child product entity per combination.
2. Data model: catalog_product_super_attribute, catalog_product_super_link
Technically, configurable products are a dedicated product type with the type code configurable, stored in the catalog_product_entity table as the type_id of the parent product. The parent product itself carries no price and no stock in the actual sense, it acts as a container connected to its child products via two central tables. The table catalog_product_super_attribute stores which attributes (by attribute_id) are configuring for a given parent product, while catalog_product_super_link represents the actual parent-child relationship between product_id (parent) and parent_id (child reference).
Every child product is a full-fledged simple product with its own entry in catalog_product_entity_int, where the concrete value of the configuring attribute (for example the option ID of color=red) is stored as an EAV row. When loading the product detail page, Magento reads out all child products via the type instance mechanism of the Configurable class, builds the JSON configuration for the frontend from it, and determines which attribute-value combinations actually exist. This indirection via EAV tables instead of a flat column is the reason why configurable products noticeably generate more joins, and thus more load time in the admin grid, than pure simple products once combination counts grow large.
For PHP 8.4 developers it is important to know that programmatic access to this data model should not happen directly through the tables but through Service Contracts such as ProductRepositoryInterface combined with the extension attributes configurable_product_options and configurable_product_links. Direct SQL write access to catalog_product_super_attribute bypasses indexer triggers, event observers and cache invalidation and reliably leads to inconsistent configurable products in the frontend.
3. Attribute set design: choosing configuring attributes correctly
Only attributes of input type dropdown or visual swatch/text swatch can be used as a configuring attribute for configurable products, because Magento needs a fixed, finite value list (eav_attribute_option) to build the combination matrix from it. Free-text fields, multiple selects or price tables are ruled out from the start. In addition, the attribute must be globally scoped (no website- or store-specific value), otherwise different configurations arise per store that Magento cannot cleanly represent in administration.
The second design decision concerns used_in_product_listing: attributes needed for filter or display logic on category and search result pages should be checked for this property, because otherwise they force additional EAV joins in the product list on every category page load, regardless of whether the attribute is configuring. In practice it is proven to strictly separate configuring attributes from purely informational attributes and to mark only the truly purchase-decisive characteristics (typically size and color) as configuring.
The practical rule of thumb is: no more than two, in exceptional cases three, configuring attributes per attribute set. With two attributes of eight values each, at most 64 combinations arise, a value that can still be well maintained and clearly displayed in the admin grid. With three attributes of eight values each, there are already 512 theoretical combinations, of which in reality often only a fraction is orderable. Every additional configuring attribute multiplies the number of potential child products and thus the maintenance effort, the indexer load, and the size of the JSON configuration object on the product detail page.
4. Programmatic variation generation in detail
For shops with several hundred or thousand product families, manual variation generation via the admin "Configurations" wizard is not practical. The resilient approach is a PHP 8.4 service that generates both the simple child products and the configurable parent product from an attribute matrix (for example size x color) in a single, transactionally clean pass. Magento provides for this the factory Magento\ConfigurableProduct\Helper\Product\Options\Factory, which builds the matching extension attribute objects for the parent product from an array of attribute definitions.
In the following example, constructor property promotion is used consistently to declare repository, factory and options factory as readonly properties. The service accepts already created child product IDs (these are created beforehand via the same ProductRepositoryInterface for every combination from the matrix) and links them via the extension attributes configurable_product_options and configurable_product_links to the new parent product. This separation of child creation and linking allows errors in individual combinations to be handled in isolation without aborting the entire import.
<?php
declare(strict_types=1);
namespace Mironsoft\ConfigurableGenerator\Service;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\ConfigurableProduct\Helper\Product\Options\Factory as OptionsFactory;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
/**
* Service for programmatic generation of a configurable product
* including the full variation matrix from simple child products.
*/
final class VariationMatrixGenerator
{
/**
* @param ProductRepositoryInterface $productRepository read/write access to products
* @param ProductInterfaceFactory $productFactory factory for new product entities
* @param OptionsFactory $optionsFactory factory for configurable attribute options
* @param Configurable $configurableType product type model for child product linking
*/
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
private readonly ProductInterfaceFactory $productFactory,
private readonly OptionsFactory $optionsFactory,
private readonly Configurable $configurableType,
) {
}
/**
* Creates a configurable parent product covering all combinations
* of the given configuring attributes and pre-created child products.
*
* @param string $parentSku SKU of the configurable parent to create
* @param array<int, array{attribute_id:int, values:array<int, array{value_index:int}>}> $configurableAttributes
* @param int[] $childProductIds already created simple product IDs, one per combination
* @return void
* @throws CouldNotSaveException when the parent product cannot be persisted
* @throws NoSuchEntityException when the parent product cannot be reloaded after saving
*/
public function generate(string $parentSku, array $configurableAttributes, array $childProductIds): void
{
$parent = $this->productFactory->create();
$parent->setSku($parentSku);
$parent->setTypeId(Configurable::TYPE_CODE);
$parent->setAttributeSetId((int) $parent->getDefaultAttributeSetId());
$configurableOptions = $this->optionsFactory->create($configurableAttributes);
$extensionAttributes = $parent->getExtensionAttributes();
$extensionAttributes->setConfigurableProductOptions($configurableOptions);
$extensionAttributes->setConfigurableProductLinks($childProductIds);
$parent->setExtensionAttributes($extensionAttributes);
$this->productRepository->save($parent);
}
}
5. Performance with many combinations: indexer and EAV joins
The catalog_product_attribute indexer is particularly relevant for configurable products, because on every change to a child product (price, visibility, attribute value) it must recalculate which combinations are available in the frontend. In shops with 10,000 or more SKUs where many parent products have 20 to 50 child products, every bulk change (for example a price import) leads to a considerable number of affected rows in the MView changelog table. If the indexer runs in "Update on Save" mode, a large import blocks the saving process until all dependent parent products are reindexed, which can lead to timeouts in cron jobs.
The "Update by Schedule" mode decouples this work into an asynchronous consumer queue processed via cron. For very large catalogs with many child products per parent product, it is additionally advisable to increase the MView batch size (mview_grid_batch_size in the configuration) and to increase the number of parallel consumer processes for indexerProductAttribute, so that the queue does not fall behind incoming changes. In practice, parent products with more than 80 to 100 child products noticeably increase the reindex effort per product, because every EAV value resolution happens individually per child.
The admin product grid also suffers from overly large configurations: the grid indexer must evaluate all child products for every parent product to correctly display total stock and price range. With parent products having several hundred child products (for example with three configuring attributes of high cardinality), loading the product list in the backend becomes noticeably slower. A regular look at bin/magento indexer:status and the MView queue length belongs in standard monitoring for configurable products with a high variation count.
# Check status of all relevant indexers
bin/magento indexer:status
# Switch to schedule mode (recommended from about 5,000 SKUs)
bin/magento indexer:set-mode schedule catalog_product_attribute
bin/magento indexer:set-mode schedule cataloginventory_stock
bin/magento indexer:set-mode schedule catalogsearch_fulltext
# Start the consumer for the attribute indexer queue with a message limit
bin/magento queue:consumers:start indexerProductAttribute --max-messages=5000
# Force a full reindex after a large attribute or price change
bin/magento indexer:reindex catalog_product_attribute
# Check cron status, since scheduled indexers run via cron
bin/magento cron:run --group=index
6. Frontend performance: JSON config payload and swatches
On the product detail page, Magento builds for configurable products a JSON configuration object that contains, for every attribute-value combination, the associated child product ID, the price and the image reference. With two configuring attributes with a manageable number of values, this object stays small, usually a few kilobytes. With three attributes of high cardinality or several hundred child products, however, the payload can grow to several hundred kilobytes, which directly worsens the page's time to interactive, because the browser must parse the complete object before the first swatch selection responds.
Hyva solves the rendering of this configuration object without jQuery and without Knockout.js, by having an Alpine.js component take over the server-rendered JSON directly as an x-data initial value. Resolving which swatch combination is currently available happens entirely client-side via an index that Magento delivers in the JSON, so that no additional requests are needed for every selection. What matters for performance is including only the fields actually needed (price, defaultPrice, images, index) in the payload and removing optional metadata such as detailed attribute descriptions from the configuration via layout XML if they are not displayed in the frontend.
With very large configurations (more than 150 to 200 child products), it is also worth virtualizing the rendering of the swatch buttons or switching to a combined dropdown display for the second attribute once the first attribute has been selected. This way Alpine.js does not need to render all theoretical combinations into the DOM simultaneously, only the subset actually relevant after the first selection, which is noticeably faster especially on mobile devices.
<?php
/** @var \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable $block */
/** @var \Magento\Framework\Escaper $escaper */
$configObject = $block->getJsonConfig();
?>
<div x-data="initConfigurableProduct(<?= /* @noEscape */ $configObject ?>)" class="configurable-swatch-wrapper">
<template x-for="attribute in Object.values(config.attributes)" :key="attribute.id">
<div class="mb-4">
<span class="font-semibold text-sm" x-text="attribute.label"></span>
<div class="flex flex-wrap gap-2 mt-2">
<template x-for="option in attribute.options" :key="option.id">
<button
type="button"
@click="selectOption(attribute.id, option.id)"
:disabled="!isSalable(attribute.id, option.id)"
:class="{ 'opacity-30 cursor-not-allowed': !isSalable(attribute.id, option.id), 'ring-2 ring-orange-600': isSelected(attribute.id, option.id) }"
class="border rounded px-3 py-1.5 text-sm"
x-text="option.label"
></button>
</template>
</div>
</div>
</template>
</div>
<script>
function initConfigurableProduct(config) {
return {
config: config,
selected: {},
selectOption(attributeId, optionId) { this.selected[attributeId] = optionId; },
isSelected(attributeId, optionId) { return this.selected[attributeId] === optionId; },
isSalable(attributeId, optionId) {
return Object.values(this.config.index).some((product) => product[attributeId] === optionId);
},
};
}
</script>
<?php /* @escapeNotVerified */ $hyvaCsp->registerInlineScript(); ?>
7. Caching strategies: Full Page Cache per parent product
The Full Page Cache stores the product detail page of a configurable parent product as a single cache entry, regardless of how many child products it has. This means that a price or stock change on a single child product actually makes the cached page of the parent product outdated, but Magento by default only invalidates the cache tags of the directly changed product. Without an additional measure, this can lead to a customer seeing a variation as available on a cached PDP that has just sold out.
The robust solution is a plugin on getIdentities() that, for configurable products, additionally adds the cache tags of all child products to the identities of the parent product. This way Magento registers the FPC page of the parent product as dependent on every individual child, and a change to any child product correctly triggers invalidation of the parent PDP. This extension should be registered as an independent plugin in di.xml to leave the core logic of Magento\Catalog\Model\Product unchanged.
<?php
declare(strict_types=1);
namespace Mironsoft\ConfigurableCache\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
/**
* Extends the cache identities of a configurable product with the
* identities of all its child products, so FPC invalidation on the
* parent PDP fires correctly when a child price or stock changes.
*/
final class AddChildProductCacheTagsPlugin
{
/**
* @param Configurable $configurableType product type model to load linked children
*/
public function __construct(
private readonly Configurable $configurableType,
) {
}
/**
* Adds the cache tag of every child product to the parent's identities.
*
* @param ProductInterface $subject the configurable parent product
* @param string[] $result cache tags resolved by Magento core
* @return string[]
*/
public function afterGetIdentities(ProductInterface $subject, array $result): array
{
if ($subject->getTypeId() !== Configurable::TYPE_CODE) {
return $result;
}
$childIds = $this->configurableType->getChildrenIds($subject->getId())[0] ?? [];
foreach ($childIds as $childId) {
$result[] = ProductInterface::CACHE_TAG . '_' . $childId;
}
return array_unique($result);
}
}
8. Stock and salable quantity per variation
Since Multi Source Inventory, every child product of a configurable parent product has its own salable quantity per source and stock, instead of a single global stock value. This allows precise statements such as: size M in red is salable at warehouse A but not at warehouse B, without affecting other size-color combinations of the same parent product. For configurable products this means the availability check must happen at the child product level, not at the parent product level, which itself carries no own stock.
In the frontend, sold-out combinations must be either hidden or disabled from the swatch or dropdown selection, so customers cannot select a non-orderable variation. Magento provides for this the index within the JSON configuration object, which also accounts for availability per child product ID. For custom ViewModels, for example to build a compact stock overview in admin or a custom availability display, the combination of Configurable::getUsedProducts() and the MSI service GetProductSalableQuantityInterface is the recommended approach, because both are stable and version-safe via Service Contracts.
<?php
declare(strict_types=1);
namespace Mironsoft\ConfigurableStock\ViewModel;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\InventorySalesApi\Api\GetProductSalableQuantityInterface;
/**
* Determines for every child product of a configurable product whether
* it is currently salable, so out-of-stock variants can be excluded
* from the swatch or dropdown selection on the storefront.
*/
final class SalableVariantsViewModel implements ArgumentInterface
{
/**
* @param Configurable $configurableType product type model to load all child products
* @param GetProductSalableQuantityInterface $getSalableQuantity MSI service for salable quantity per SKU and website
*/
public function __construct(
private readonly Configurable $configurableType,
private readonly GetProductSalableQuantityInterface $getSalableQuantity,
) {
}
/**
* Returns a map of SKU to salable quantity for all children of the given parent.
*
* @param ProductInterface $parent configurable parent product
* @param int $websiteId website ID used as MSI context
* @return array<string, float>
*/
public function getSalableQuantities(ProductInterface $parent, int $websiteId): array
{
$result = [];
foreach ($this->configurableType->getUsedProducts($parent) as $child) {
$result[$child->getSku()] = $this->getSalableQuantity->execute($child->getSku(), $websiteId);
}
return $result;
}
}
9. Configurable products compared: Configurable vs. Bundle vs. Grouped
All three product types solve the basic problem of "offering several product variations under one roof", but with fundamentally different data storage, pricing logic and indexing load. The following table summarizes when configurable products are the technically more suitable choice compared to bundle and grouped.
| Dimension | Configurable | Bundle | Grouped |
|---|---|---|---|
| Variation generation | Own simple child product per combination, linked via super_link | Options as selection groups, no physical variation needed | List of independent, already existing products |
| Pricing logic | Price per child product, optionally with surcharge per attribute value | Fixed, dynamic, or calculated per option | Every product keeps its own, independent price |
| Indexing effort | High with many child products, EAV joins per combination | Moderate, depends on the number of linked options | Low, every product is indexed independently |
| Use case | Size, color, material as clearly delimitable purchase variations | Sets, configurators with optional additional components | Related individual products on a shared overview page |
In practice, these product types are rarely used in isolation. A common pattern is a configurable product for size and color, whose individual child products are in turn combined as part of a bundle set with accessories. What matters is clarifying early during data modeling at which level the pricing logic should take effect, since subsequently switching product type in grown catalogs with order history is technically demanding and error-prone.
10. Summary
Configurable products solve a clearly outlined problem in Magento 2: bundling variations such as size and color as independent, stock-managed simple products under a shared parent product. The data model made of catalog_product_super_attribute and catalog_product_super_link is deliberately kept generic, which brings flexibility in attribute choice but also means that performance aspects such as indexer load, JSON payload size and FPC invalidation must be actively considered once the number of combinations grows.
The biggest lever lies in discipline around attribute set design: no more than two to three configuring attributes, programmatic variation generation via Service Contracts instead of manual admin maintenance, and monitoring of indexer status and MView queue length from several thousand SKUs onward. Anyone who additionally propagates cache tags for child products correctly and queries salable quantity per variation via MSI avoids the typical symptoms of large configurable catalogs: slow admin grids, outdated prices in the Full Page Cache, and sold-out variations incorrectly displayed as available.
Configurable Products in Magento 2: The Essentials at a Glance
Data model & attributes
catalog_product_super_attribute and catalog_product_super_link connect parent and child products. Only dropdown or swatch attributes can be configuring.
Variation generation
Use Options\Factory and ProductRepositoryInterface for bulk-capable, programmatic generation instead of manual admin maintenance.
Indexer & performance
Run catalog_product_attribute in schedule mode, monitor the MView queue, limit child products per parent product.
Caching & stock
Propagate child product cache tags to the parent product, query salable quantity per child via MSI services.
11. FAQ: Configurable Products in Magento 2
1Difference between configurable and bundle products?
2How many configuring attributes are practical?
3Why must a configuring attribute be dropdown or swatch?
4How do I generate variations programmatically?
5Which indexer is most important?
6From how many SKUs should I switch to schedule mode?
7How large can the JSON config payload get?
8How does Full Page Cache work here?
9How are sold-out variations handled?
10When to use Grouped instead of Configurable?
Mironsoft
Magento 2 development, performance optimization and Hyva theme implementation
Configurable products that stay performant even at 10,000+ SKUs?
We analyze your attribute set design, optimize programmatic variation generation, and tune indexers and Full Page Cache for large combination counts, so configurable products stay stable and fast in Magento 2 and Hyva.
Attribute set audit
Reviewing configuring attributes and preventing combination explosion before launch
Variation generation & migration
Building bulk-capable Service Contracts for simple and configurable products
Indexer and cache tuning
Setting up schedule mode, MView queues and cache tag propagation for child products