Product Variants Guide
Overview
The product variants system allows you to manage multiple variations of a single product (e.g., T-shirts in different sizes and colors) through a unified Master Product system. This guide explains how to create, manage, and work with product variants using Domain-Driven Design (DDD) principles.
Architecture Concepts
Master Product vs Product
The system uses a two-tier architecture:
Master Product: The template/source of truth that defines variant configuration and default properties
- NOT directly purchasable
- Defines variant attributes (e.g., Size: S, M, L | Color: Red, Blue)
- Stores base price, cost, weight, dimensions, and images
- Can have 0 or more Product variants
Product: The actual purchasable SKU
- Always belongs to a Master Product
- Can inherit properties from Master Product or override them
- Has a specific variant selection (e.g., Size: M, Color: Red)
- Manages its own inventory and stock
Key Characteristics
- Aggregate Root Pattern: Master Product is the aggregate root that controls all variant operations
- Property Inheritance: Products inherit base properties unless explicitly overridden
- Variant Configuration: Immutable value object defining allowed attribute combinations
- Type Safety: Strong typing with Value Objects (Money, VariantConfiguration, VariantAttribute)
Variant Configuration
Creating a Variant Configuration
Variant configuration defines what attributes variants can have and their possible values. You set this when creating a product via the REST API.
Endpoint: POST /api/v1/operations/products
Request Body:
{
"name": "Cotton T-Shirt",
"sku": "TSHIRT-MASTER",
"type": "master",
"selling_price": 19.99,
"cost_price": 10.00,
"currency": "USD",
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["S", "M", "L", "XL"]
},
{
"name": "Color",
"values": ["Red", "Blue", "Green"]
}
],
"allowCombinations": true
}
}Response (201 Created):
{
"message": "Product created successfully",
"data": {
"id": 1,
"name": "Cotton T-Shirt",
"sku": "TSHIRT-MASTER",
"type": "master",
"selling_price": 19.99,
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["S", "M", "L", "XL"]
},
{
"name": "Color",
"values": ["Red", "Blue", "Green"]
}
],
"allowCombinations": true
}
}
}What This Creates:
- A master product that can have 4 sizes × 3 colors = 12 possible variant combinations
- No actual product variants yet (use the Generate Variants endpoint to create them)
Variant Configuration Properties
| Property | Type | Required | Description |
|---|---|---|---|
attributes | array | ✅ Yes | Array of variant attribute objects (e.g., Size, Color) |
attributes[].name | string | ✅ Yes | Attribute name (e.g., "Size", "Color", "Material") |
attributes[].values | array | ✅ Yes | Array of possible values (e.g., ["S", "M", "L"]) |
allowCombinations | boolean | No | Whether to allow all attribute combinations (default: true) |
Updating Variant Configuration
You can update the variant configuration using the update endpoint:
Endpoint: PUT /api/v1/operations/products/{id}
Request Body:
{
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["XS", "S", "M", "L", "XL", "XXL"]
},
{
"name": "Color",
"values": ["Red", "Blue", "Green", "Black"]
}
],
"allowCombinations": true
}
}Note: Updating variant configuration does not automatically regenerate existing variants. You must manually delete and regenerate if needed.
API Endpoints
All variant endpoints are under /api/v1/operations/master-products/{master}/variants
1. Generate All Variants
Automatically creates all possible variant combinations.
Endpoint: POST /api/v1/operations/master-products/{master}/variants/generate
Request Body:
{
"auto_generate_sku": true,
"sku_prefix": "TSHIRT",
"initial_stock": 10
}Response (201 Created):
{
"message": "Generated 12 variant(s) successfully",
"data": [
{
"id": 101,
"master_product_id": 1,
"sku": "TSHIRT-S-RED",
"name": "Cotton T-Shirt - S / Red",
"variant_selection": {
"Size": "S",
"Color": "Red"
},
"selling_price": 19.99,
"cost_price": 10.00,
"currency": "USD",
"stock_quantity": 10,
"status": "draft"
}
// ... 11 more variants
],
"meta": {
"total_generated": 12
}
}Business Logic:
- Skips combinations that already exist
- Inherits base_price, base_cost_price, base_weight from Master Product
- Auto-generates SKU if not provided:
{slug}-{Size}-{Color} - All variants start in "draft" status
Validation Rules:
- Master Product must have variant configuration
- Cannot generate if Master Product has no variant_attributes
- Generates only missing combinations (idempotent)
2. Create Single Variant
Creates a specific variant with custom properties.
Endpoint: POST /api/v1/operations/master-products/{master}/variants
Request Body:
{
"variant_selection": {
"Size": "M",
"Color": "Blue"
},
"sku": "CUSTOM-M-BLUE",
"selling_price": 24.99,
"cost_price": 12.00,
"stock_quantity": 50,
"weight": 0.35
}Response (201 Created):
{
"message": "Variant created successfully",
"data": {
"id": 102,
"master_product_id": 1,
"sku": "CUSTOM-M-BLUE",
"name": "Cotton T-Shirt - M / Blue",
"variant_selection": {
"Size": "M",
"Color": "Blue"
},
"selling_price": 24.99,
"cost_price": 12.00,
"currency": "USD",
"weight": 0.35,
"stock_quantity": 50,
"status": "draft",
"overrides": ["price", "weight"]
}
}Validation Rules:
[
'variant_selection' => ['required', 'array', 'min:1'],
'variant_selection.*' => ['required', 'string'],
'sku' => ['sometimes', 'string', 'max:100', 'unique:operations_products'],
'selling_price' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:9999999.99'],
'cost_price' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:9999999.99'],
'stock_quantity' => ['sometimes', 'integer', 'min:0', 'max:999999'],
'weight' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:99999.9999'],
]Business Logic:
- Validates variant_selection matches Master Product configuration
- Prevents duplicate variant combinations
- Auto-generates name:
{master.name} - {Size} / {Color} - Tracks which properties are overridden in
overridesfield
Error Responses:
422 Unprocessable Entity - Invalid variant selection:
{
"message": "Failed to create variant",
"error": "Invalid value 'XXL' for attribute 'Size'. Allowed values: S, M, L, XL"
}422 Unprocessable Entity - Duplicate combination:
{
"message": "Failed to create variant",
"error": "A variant with this combination already exists"
}3. List All Variants
Retrieves all variants for a Master Product.
Endpoint: GET /api/v1/operations/master-products/{master}/variants
Query Parameters:
page: Page number (default: 1)per_page: Items per page (default: 50)status: Filter by status (active, draft, discontinued)include: Relationships to include (category, supplier, inventory)
Response (200 OK):
{
"data": [
{
"id": 101,
"sku": "TSHIRT-S-RED",
"name": "Cotton T-Shirt - S / Red",
"variant_selection": {
"Size": "S",
"Color": "Red"
},
"selling_price": 19.99,
"stock_quantity": 45,
"status": "active"
}
],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 12,
"last_page": 1
}
}4. Get Available Variant Values
Shows which variant combinations are still available (not yet created).
Endpoint: GET /api/v1/operations/master-products/{master}/variants/available
Response (200 OK):
{
"data": {
"Size": {
"used_values": ["S", "M"],
"available_values": ["L", "XL"]
},
"Color": {
"used_values": ["Red", "Blue"],
"available_values": ["Green"]
}
},
"meta": {
"total_combinations": 12,
"used_combinations": 6,
"available_combinations": 6
}
}Use Case: Display to users which combinations can still be created.
5. Bulk Update Variant Prices
Updates selling price for multiple variants at once.
Endpoint: POST /api/v1/operations/master-products/{master}/variants/bulk-update-prices
Request Body:
{
"action": "set_price",
"price": 24.99,
"filters": {
"variant_selection": {
"Size": "L"
}
}
}Alternative Actions:
Percentage increase:
{
"action": "increase_percentage",
"percentage": 10,
"filters": {
"variant_selection": {
"Color": "Red"
}
}
}Response (200 OK):
{
"message": "Updated prices for 3 variants",
"data": {
"updated_count": 3,
"affected_variants": [101, 102, 103]
}
}6. Bulk Update Variant Stock
Updates stock quantity for multiple variants.
Endpoint: POST /api/v1/operations/master-products/{master}/variants/bulk-update-stock
Request Body:
{
"action": "set_quantity",
"quantity": 100,
"filters": {
"variant_selection": {
"Size": "M"
}
}
}Response (200 OK):
{
"message": "Updated stock for 3 variants",
"data": {
"updated_count": 3,
"total_stock_added": 300
}
}7. Delete All Variants
Deletes all variants from a Master Product.
Endpoint: DELETE /api/v1/operations/master-products/{master}/variants
Request Body:
{
"reason": "Updating product configuration"
}Response (200 OK):
{
"message": "Deleted 12 variants successfully",
"data": {
"deleted_count": 12
}
}Business Rules:
- CANNOT delete variants with active inventory
- Requires a reason for audit trail
- Logs deletion to application logs
Error Response (422):
{
"message": "Cannot delete variants with active inventory. Found 5 variants with inventory."
}Property Inheritance
Products can inherit properties from their Master Product or override them.
Inheritable Properties
| Property | Master Field | Product Field | Default Behavior |
|---|---|---|---|
| Price | base_price | selling_price | Inherit if NULL |
| Cost | base_cost_price | cost_price | Inherit if NULL |
| Weight | base_weight | weight | Inherit if NULL |
| Dimensions | base_dimensions | dimensions | Inherit if NULL |
| Currency | currency | currency | Inherit if NULL |
| Images | images | images | Merge both |
Inheritance Example
Master Product:
{
"id": 1,
"name": "Cotton T-Shirt",
"base_price": 19.99,
"base_cost_price": 10.00,
"base_weight": 0.30,
"currency": "USD"
}Product (inheriting):
{
"id": 101,
"master_product_id": 1,
"selling_price": null, // Will inherit 19.99
"cost_price": null, // Will inherit 10.00
"weight": null // Will inherit 0.30
}Product (overriding):
{
"id": 102,
"master_product_id": 1,
"selling_price": 24.99, // Override: Premium size
"cost_price": 12.00, // Override
"weight": 0.35, // Override: Larger size
"overrides": ["price", "cost_price", "weight"]
}Checking Overrides
When you retrieve a product via the API, the response includes which properties are overridden:
GET /api/v1/operations/products/102Response:
{
"data": {
"id": 102,
"master_product_id": 1,
"selling_price": 24.99,
"cost_price": 12.00,
"weight": 0.35,
"overrides": ["price", "cost_price", "weight"],
"effective_values": {
"price": 24.99,
"cost_price": 12.00,
"weight": 0.35
}
}
}Business Rules & Validation
Variant Creation Rules
Master Product must have variant configuration
variant_attributesfield must be set- Configuration must have at least one attribute
Variant selection must match configuration
- All required attributes must be present
- All values must be valid for their attributes
- No extra attributes allowed
No duplicate combinations
- Each variant selection must be unique within a Master Product
- Comparison is based on the complete combination
SKU uniqueness
- SKUs must be unique across all products (tenant-wide)
- Auto-generated SKUs follow pattern:
{master.slug}-{attr1}-{attr2}
Deletion Rules
Cannot delete variants with inventory
- Checks all warehouses for stock records
- Must transfer/remove inventory first
Requires audit reason
- Deletion reason logged for compliance
- Helps track why variants were removed
Price & Cost Rules
Monetary values use Money Value Object
- Prevents floating-point precision errors
- Stored as cents in database
- Currency awareness enforced
Validation limits
- Min: 0.00 (cannot be negative)
- Max: 9,999,999.99
- Decimal precision: 2 places
Domain Model Reference
MasterProduct Model
File: app/Modules/Operations/Domain/Models/MasterProduct.php
Key Methods:
// Variant detection
hasVariants(): bool // Check if variants enabled
isSingleProduct(): bool // Check if standalone product
isEmpty(): bool // Check if no products exist
// Variant operations (Aggregate Root)
addVariant(array $selection, array $overrides): Product
generateAllVariants(array $defaults): Collection
updateVariantConfiguration(VariantConfiguration $config, bool $regenerate): void
deleteAllVariants(string $reason): int
// Read-only access
getVariants(): Collection
getAvailableVariantValues(): array
// Aggregations
getTotalStock(): int
getLowestPrice(): ?Money
getHighestPrice(): ?Money
getPriceRange(): arrayProduct Model
File: app/Modules/Operations/Domain/Models/Product.php
Key Methods:
// Variant detection
isVariant(): bool // Has sibling variants
isStandalone(): bool // Only product of master
// Property inheritance
getPrice(): Money // Get effective price (with inheritance)
getCostPrice(): ?Money // Get effective cost
getWeight(): ?float // Get effective weight
getEffectiveDimensions(): ?ProductDimensions
getCurrency(): string
getAllImages(): array // Merge master + product images
// Override management
isOverridden(string $property): bool
overridePrice(float $price): void
clearPriceOverride(): voidVariantConfiguration Value Object
File: app/Modules/Operations/Domain/ValueObjects/VariantConfiguration.php
Properties:
attributes: Array of VariantAttribute objectsallowCombinations: Whether to allow all combinations
Methods:
fromArray(array $data): self
toArray(): array
getAttributes(): array
getAttribute(string $name): ?VariantAttribute
isValidCombination(array $values): bool
getTotalCombinations(): int
getAllCombinations(): array // Cartesian productVariantAttribute Value Object
File: app/Modules/Operations/Domain/ValueObjects/VariantAttribute.php
Properties:
name: Attribute name (e.g., "Size")values: Array of possible values (e.g., ["S", "M", "L"])
Methods:
fromArray(array $data): self
getName(): string
getValues(): array
hasValue(string $value): bool
getValueCount(): intBest Practices
1. Use Variant Creation Endpoints
Always create variants through the proper API endpoints:
# ✅ CORRECT: Use variant creation endpoint
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "L",
"Color": "Blue"
},
"selling_price": 24.99
}
# ❌ WRONG: Don't try to create products directly as variants
POST /api/v1/operations/products
{
"master_product_id": 1,
"variant_selection": {"Size": "L", "Color": "Blue"}
}
# This bypasses business rules and validation2. Leverage Property Inheritance
Don't override unless necessary:
# ✅ CORRECT: Inherit common properties (no overrides)
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "M",
"Color": "Red"
}
# Inherits base_price, base_cost, base_weight from master
}
# Only override when different
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "XL",
"Color": "Red"
},
"selling_price": 24.99 # Premium size override
}3. Validate Before Generation
Check how many combinations will be created:
# Get available variant values and total combinations
GET /api/v1/operations/master-products/1/variants/availableResponse:
{
"meta": {
"total_combinations": 12,
"used_combinations": 0,
"available_combinations": 12
}
}If total_combinations > 100, consider whether you really need all combinations or if you should create variants selectively.
4. Use Bulk Operations
For multiple updates, use bulk endpoints:
# ✅ CORRECT: Single request to update multiple variants
POST /api/v1/operations/master-products/1/variants/bulk-update-prices
{
"action": "increase_percentage",
"percentage": 10
}
# ❌ INEFFICIENT: Multiple individual requests
PUT /api/v1/operations/products/101 {"selling_price": 21.99}
PUT /api/v1/operations/products/102 {"selling_price": 21.99}
PUT /api/v1/operations/products/103 {"selling_price": 21.99}
# ... (inefficient for many variants)5. Use Proper Number Formats
Always use numeric values for prices in the API:
# ✅ CORRECT: Numeric values
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {"Size": "M"},
"selling_price": 19.99,
"cost_price": 10.50
}
# ❌ WRONG: String values
{
"selling_price": "19.99", # Don't use strings
"cost_price": "$10.50" # Don't include currency symbols
}The API automatically handles the Money Value Object conversion internally.
Common Use Cases
Use Case 1: Apparel with Size & Color
Step 1: Create Master Product with Variant Configuration
POST /api/v1/operations/products
{
"name": "Cotton T-Shirt",
"sku": "TSHIRT-MASTER",
"type": "master",
"selling_price": 19.99,
"cost_price": 10.00,
"currency": "USD",
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["S", "M", "L", "XL"]
},
{
"name": "Color",
"values": ["Red", "Blue", "Green"]
}
],
"allowCombinations": true
}
}Step 2: Generate All Variant Combinations
POST /api/v1/operations/master-products/1/variants/generate
{
"auto_generate_sku": true,
"sku_prefix": "TSHIRT",
"initial_stock": 10
}Result: Creates 4 sizes × 3 colors = 12 variants
Use Case 2: Electronics with Storage & Color
Step 1: Create Master Product
POST /api/v1/operations/products
{
"name": "Smartphone X",
"sku": "PHONE-X-MASTER",
"type": "master",
"currency": "USD",
"variant_attributes": {
"attributes": [
{
"name": "Storage",
"values": ["64GB", "128GB", "256GB"]
},
{
"name": "Color",
"values": ["Black", "White", "Blue"]
}
]
}
}Step 2: Create Variants with Different Pricing
# 64GB Black - Base model
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Storage": "64GB",
"Color": "Black"
},
"selling_price": 699.00
}
# 128GB Black - Mid-tier
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Storage": "128GB",
"Color": "Black"
},
"selling_price": 799.00
}
# 256GB Black - Premium
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Storage": "256GB",
"Color": "Black"
},
"selling_price": 999.00
}Use Case 3: Furniture with Custom Dimensions
Step 1: Create Master Product
POST /api/v1/operations/products
{
"name": "Office Desk",
"sku": "DESK-MASTER",
"type": "master",
"selling_price": 299.00,
"currency": "USD",
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["Small", "Medium", "Large"]
},
{
"name": "Finish",
"values": ["Oak", "Walnut", "Black"]
}
]
}
}Step 2: Create Variant with Custom Dimensions
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "Small",
"Finish": "Oak"
},
"dimensions": {
"length": 120,
"width": 60,
"height": 75,
"dimension_unit": "cm"
},
"weight": 25.5
}Troubleshooting
Issue: "Variants not enabled"
Error: Cannot add variant to master product without variant configuration
Solution: Create or update the product with variant_attributes:
PUT /api/v1/operations/products/1
{
"variant_attributes": {
"attributes": [
{
"name": "Size",
"values": ["S", "M", "L"]
}
]
}
}Issue: "Invalid variant selection"
Error: Invalid value 'XXL' for attribute 'Size'. Allowed values: S, M, L, XL
Solution: Check available values first, then use only configured values:
# Check available values
GET /api/v1/operations/master-products/1/variants/available
# Create variant with valid value
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "L" # ✅ Valid value
}
}Issue: "Duplicate variant"
Error: A variant with this combination already exists
Solution: Check existing variants before creating:
# List existing variants
GET /api/v1/operations/master-products/1/variants
# Create only if combination doesn't exist
POST /api/v1/operations/master-products/1/variants
{
"variant_selection": {
"Size": "M",
"Color": "Blue"
}
}Issue: "Cannot delete variants with inventory"
Error: Found 5 variants with inventory
Solution: Adjust inventory to zero first, then delete:
# Option 1: Transfer inventory to another variant
POST /api/v1/operations/inventory/transfer
{
"product_id": 101,
"from_warehouse_id": 1,
"to_warehouse_id": 1,
"quantity": 50,
"reason": "Consolidating before deletion"
}
# Option 2: Adjust inventory to zero
POST /api/v1/operations/inventory/101/adjust
{
"quantity": -50,
"reason": "Clearing before deletion"
}
# Then delete variants
DELETE /api/v1/operations/master-products/1/variants
{
"reason": "Reconfiguring product"
}Related Documentation
- Pricing Strategy Guide - Learn about Money Value Object and pricing
- Product Attributes Guide - EAV system for dynamic attributes
- Inventory Management - Stock control for variants
- DDD Module Blueprint - Architecture patterns