Custom Product Options in Laravel: How I Handle Size, Colour and Stock

Custom product options in Laravel — size, colour, material, or any other configurable attribute — need their own validation, pricing, and stock logic layered on top of the base product record. I handle every configurable option as a distinct, sellable combination, not as a cosmetic label on a single generic product, because that's the only way pricing and stock stay accurate once a product has more than one variant.
Custom product options in Laravel — size, colour, material, or any other configurable attribute — need their own validation, pricing, and stock logic layered on top of the base product record. I handle every configurable option as a distinct, sellable combination, not as a cosmetic label on a single generic product, because that's the only way pricing and stock stay accurate once a product has more than one variant.
Here's exactly how I structure required options, valid-selection checks, option-level pricing and stock, and the configuration fingerprint that keeps identical selections merging correctly into one cart line instead of duplicating.
What a Configurable Product Actually Is
A configurable product is a base product definition — name, description, base images — paired with one or more option groups (size, colour, material) whose specific combinations each resolve to their own price and stock level. A medium blue t-shirt and a large blue t-shirt are the same product conceptually, but two entirely separate sellable units as far as pricing and inventory are concerned.
I model this in Laravel as a product with a related set of option values, and a separate table of valid combinations — each combination carrying its own SKU, price override (if any), and stock count. This structure means the product itself stays simple while the combinations carry the commercially important detail.
Option groups themselves are also modelled as their own records rather than hard-coded columns on the product table. A "size" option group and a "colour" option group are reusable across many products, and a product can have one, two, or several option groups depending on what it actually needs — a plain candle might have no configurable options at all, while a jacket might have size, colour, and material all at once. Keeping option groups as their own entity means adding a new kind of configurable attribute to the catalogue later doesn't require a schema change, just a new option group and its values.
Required Options and Valid-Selection Checks
Required options and valid-selection checks stop a customer from adding an incomplete or invalid configuration to their cart. If a product requires both a size and a colour selection, the add-to-cart action is blocked until both are chosen — and once both are chosen, I verify that specific combination is actually a real, sellable one, not just that each individual option value exists somewhere in the product's option list.
This distinction matters because not every combination of valid individual options is itself valid. A product might offer red, blue, and green in small and medium, but the green option might only ever have been stocked in medium — red-medium, blue-medium, and green-medium are valid combinations, but green-small never was. I check the combination as a whole against what's actually been defined as sellable, not the individual option values in isolation.
This check runs before the item ever reaches the cart, which matters for the customer experience as much as for data integrity. A customer who's told immediately that green isn't available in small can adjust their selection and keep shopping. A customer who discovers the same problem only after adding the item, or worse, only at checkout, has already invested effort in a purchase that was never going to complete — and that's exactly the kind of friction that causes someone to abandon the store altogether rather than simply pick a different colour.
- Customer selects a value for each required option group.
- The system checks whether this specific combination exists as a defined, sellable configuration.
- If the combination is invalid or has no stock, the customer sees a clear message before anything is added to the cart.
- If valid, the specific configuration — not just the base product — is what gets added.
Not every combination of valid individual options is itself a valid, sellable configuration.Option-Level Pricing
Option-level pricing means a specific configuration can carry its own price, separate from the base product price — a larger size or a premium material might cost more, and that difference needs to be resolved automatically rather than handled through manual price overrides scattered across a spreadsheet. I store a price adjustment (or an absolute override) per configuration, and the cart's pricing service — the same server-authoritative pricing I use throughout the rest of the cart — resolves the final price by combining the base price with whatever adjustment the selected configuration carries.
This keeps pricing logic in one place. Whether a customer is buying the base configuration or a premium one, the price shown is always calculated the same way, from the same source, at the same moment — never a value cached from an earlier page load. It's the same server-authoritative principle I've written about for Laravel cart pricing and stock accuracy generally — configurable products are simply the case where that principle has to reach one level deeper, into the specific combination rather than the product as a whole.
Option-Level Stock
Option-level stock means every configuration tracks its own inventory count independently — a store can be fully stocked on medium-blue while completely sold out of large-blue, and the system needs to reflect that difference precisely rather than reporting stock at the product level as a single aggregate number. I attach the stock count to the specific configuration record, not the parent product, so a stock check always resolves against the exact combination a customer has selected.
This is the same stock-aware validation discipline I apply throughout a Laravel cart — checked live as the cart changes, and checked again independently at checkout — but here it's applied per configuration rather than per product, since that's the level at which stock actually varies.
Row-level locking at order time applies at this same configuration level too. If two customers are both trying to buy the last unit of medium-blue, the lock has to be scoped to that specific configuration's stock row, not the parent product's — locking the whole product would needlessly block a customer buying large-red at the exact same moment, even though the two orders don't actually compete for the same inventory.
Configuration Fingerprints and Merged Cart Lines
A configuration fingerprint is a stable, deterministic identifier generated from a product ID plus its full set of selected option values, used to decide whether a new cart addition should create a new line or merge into an existing one. I generate this fingerprint the same way every time — sorting option values into a consistent order before hashing them — so that selecting "blue, then medium" and "medium, then blue" produce the identical fingerprint, even though a customer might pick the options in a different order on the page.
When an item is added to the cart, I check whether its fingerprint already exists as a line in that cart. If it does, the quantity merges into the existing line. If it doesn't, a new line is created. This is what keeps a cart clean and accurate: a customer adding the same size-and-colour combination twice ends up with one line showing quantity two, not two separate lines each showing quantity one.
Getting the fingerprint calculation wrong is a subtle bug that's easy to miss in testing but obvious to a customer in production. If option order isn't normalised before hashing, the same product added via two slightly different UI paths — a quick "buy again" button versus the full product page — can generate two different fingerprints for what should be identical cart lines. The result is a cart that looks duplicated even though nothing about the customer's actual order is wrong, which erodes trust in the store even when the underlying order ends up correct.
Identical configurations always generate the same fingerprint, so they merge into one line rather than duplicating.Why This Matters for Your Catalogue
Getting configuration logic right matters most for catalogues where option combinations genuinely affect price, stock, or availability — apparel with size and colour variants, furniture with material and finish choices, or any product line where "the same item" actually means several distinct sellable units. A catalogue without meaningful configurable options doesn't need this complexity; a single product record with a flat price and one stock count is simpler and entirely appropriate there.
Where it does apply, getting it wrong shows up as real operational problems: a customer paying the wrong price for a specific size, an order accepted for a configuration that was actually out of stock, or a cart showing three separate lines for what should have been one item at quantity three. I build this layer specifically so those failure modes can't happen, not just so the product options look correct on the page.
This same configuration logic scales to more complex catalogues too — a multi-vendor marketplace, for instance, often has the added wrinkle of the same conceptual product being sold by different vendors at different prices and stock levels, which is really just another dimension added to the same fingerprint-and-combination structure described here. I've covered that specific extension in building a multi-vendor marketplace, where vendor identity becomes one more part of what makes a configuration unique.
Frequently Asked Questions
What happens if a customer picks a combination of options that was never meant to be sold?
The system checks the full combination against what's actually been defined as a valid, sellable configuration — not just whether each individual option value exists. An invalid combination is rejected with a clear message before it can be added to the cart.
Does every size or colour variant need its own price?
No. Configurations only need a price adjustment if that specific combination genuinely costs more or less than the base product. Configurations without an adjustment simply use the base price.
How does the cart know two identical selections are the same item?
A configuration fingerprint is generated from the product and its selected option values in a consistent order, so identical selections always produce the same fingerprint regardless of what order the options were picked in. Matching fingerprints merge into one cart line.
Can stock run out for just one colour while other colours are still available?
Yes. Stock is tracked at the level of each specific configuration, not the product as a whole, so one colour or size can sell out completely while others remain fully available.
Do I need this level of complexity if my products don't have many variants?
Not necessarily. This structure earns its complexity when configurations genuinely affect price, stock, or availability. A simpler catalogue with flat pricing and a single stock count per product doesn't need it.
If your store sells configurable products and you're seeing pricing or stock mismatches between variants, I can help you scope a cart engine that handles configuration logic correctly from the start.
See how I build Laravel carts with configurable productsReady to talk through your product configuration requirements?
Book a scoping call