Software Archetypes

The Product Archetype

Based on "Enterprise Patterns and MDA" by Jim Arlow & Ila Neustadt

Problem

Products in a catalog have complex structures:

  • Categories with shared attributes: All laptops have RAM and storage; all t-shirts have size and color
  • Catalog vs physical items: "iPhone 15 Pro 256GB Black" (catalog) vs "the specific phone with serial #XYZ" (instance)
  • Variations: Same product in different sizes/colors
  • Pricing complexity: Base price, discounts, customer-specific pricing
  • Hierarchical categories: Electronics → Computers → Laptops → Gaming Laptops

Concept

The Product Archetype has three layers:

  1. ProductType — Category/template defining structure (attributes, rules)
  2. Product — Catalog item you can order (SKU, price, description)
  3. ProductInstance — Physical/serialized item in inventory

Think of it as:

  • ProductType = "Laptop" (defines what attributes exist)
  • Product = "ThinkPad X1 Carbon Gen 11" (what you order)
  • ProductInstance = "Serial #ABC123" (what you receive)

Structure

parent

0..1

0..1

0..1

1

1

1

0..*

0..*

0..*

0..*

0..*

0..*

ProductType

+id: UUID

+code: String

+name: String

+parent: ProductType

+children: ProductType[]

+attributeDefinitions: AttributeDef[]

+isLeaf() : : Boolean

Product

+id: UUID

+sku: String

+name: String

+description: String

+productType: ProductType

+basePrice: Money

+attributes: Map

+status: ProductStatus

+isAvailable() : : Boolean

ProductInstance

+id: UUID

+product: Product

+serialNumber: String

+batchNumber: String

+manufacturedDate: Date

+expiryDate: Date

+status: InstanceStatus

AttributeDefinition

+code: String

+name: String

+dataType: DataType

+required: Boolean

+options: String[]

Attributes

ProductType

Attribute Type Required Description
id UUID Yes Unique identifier
code String Yes Unique code (LAPTOP, TSHIRT)
name String Yes Display name
description String No Category description
parent ProductType No Parent category (null = root)
attributeDefinitions AttributeDef[] No Attributes for this type
active Boolean Yes Is category active?

Product

Attribute Type Required Description
id UUID Yes Unique identifier
sku String Yes Stock Keeping Unit (unique)
name String Yes Product name
description String No Detailed description
productType ProductType Yes Category this belongs to
basePrice Money Yes Base selling price
costPrice Money No Cost/purchase price
attributes Map No Type-specific attributes
status ProductStatus Yes Draft, Active, Discontinued
weight Decimal No Weight in kg
dimensions Dimensions No L × W × H in cm

ProductInstance

Attribute Type Required Description
id UUID Yes Unique identifier
product Product Yes Which product this is
serialNumber String No Unique serial (for serialized)
batchNumber String No Batch/lot number
manufacturedDate Date No When manufactured
expiryDate Date No Expiration date (perishables)
status InstanceStatus Yes Available, Sold, Damaged, etc.

AttributeDefinition

Attribute Type Required Description
code String Yes Attribute code (RAM, SIZE)
name String Yes Display name
dataType Enum Yes String, Integer, Decimal, Boolean, Date, Enum
required Boolean Yes Must be set on Product?
options String[] No Valid values (for Enum type)
unit String No Unit of measure (GB, cm)
minValue Decimal No Minimum allowed value
maxValue Decimal No Maximum allowed value

Behaviors

Entity ProductType:
    
    function isLeaf(): Boolean
        return children is empty
    
    function isRoot(): Boolean
        return parent is None
    
    function getPath(): String[]
        // Returns ["Electronics", "Computers", "Laptops"]
        if parent is None:
            return [name]
        return parent.getPath() + [name]
    
    function getAllAttributeDefinitions(): AttributeDefinition[]
        // Inherit from ancestors + own
        inherited = parent?.getAllAttributeDefinitions() or []
        return inherited + attributeDefinitions
    
    function canHaveProducts(): Boolean
        // Only leaf types can have products directly
        return isLeaf()
    
    function getDescendants(): ProductType[]
        result = children.copy()
        for child in children:
            result += child.getDescendants()
        return result


Entity Product:
    
    function isAvailable(): Boolean
        return status == ProductStatus.Active
    
    function getAttribute(code: String): Any
        return attributes.get(code)
    
    function setAttribute(code: String, value: Any):
        definition = productType.getAllAttributeDefinitions()
            .find(d => d.code == code)
        
        require definition is not None  // Attribute must be defined
        require definition.validate(value)  // Value must be valid
        
        attributes[code] = value
    
    function validate(): ValidationResult
        errors = []
        
        for definition in productType.getAllAttributeDefinitions():
            value = attributes.get(definition.code)
            
            if definition.required and value is None:
                errors.add("Missing required attribute: " + definition.code)
            
            if value is not None and not definition.validate(value):
                errors.add("Invalid value for " + definition.code)
        
        return ValidationResult(isValid: errors.isEmpty(), errors: errors)
    
    function calculatePrice(customer: Customer): Money
        price = basePrice
        
        // Apply customer-specific pricing
        if customer.loyaltyTier == "Gold":
            price = price * 0.95  // 5% discount
        
        // Apply quantity discounts, promotions, etc.
        return price
    
    function discontinue():
        require status == ProductStatus.Active
        status = ProductStatus.Discontinued
        emit ProductDiscontinued(this)


Entity ProductInstance:
    
    function isAvailable(): Boolean
        return status == InstanceStatus.Available
    
    function isExpired(): Boolean
        return expiryDate is not None and expiryDate < today()
    
    function sell(order: Order):
        require isAvailable()
        require not isExpired()
        
        status = InstanceStatus.Sold
        soldTo = order
        soldAt = now()
        
        emit InstanceSold(this, order)
    
    function markDamaged(reason: String):
        status = InstanceStatus.Damaged
        damageReason = reason
        damagedAt = now()


Entity AttributeDefinition:
    
    function validate(value: Any): Boolean
        if value is None:
            return not required
        
        // Type check
        match dataType:
            case DataType.String:
                if not (value is String): return false
            case DataType.Integer:
                if not (value is Integer): return false
            case DataType.Decimal:
                if not (value is Number): return false
            case DataType.Boolean:
                if not (value is Boolean): return false
            case DataType.Enum:
                if value not in options: return false
        
        // Range check
        if minValue is not None and value < minValue:
            return false
        if maxValue is not None and value > maxValue:
            return false
        
        return true

Examples

Setting up a product catalog

// ProductType hierarchy: Electronics > Laptops
electronics = new ProductType(code: "ELECTRONICS", name: "Electronics")
laptops = new ProductType(code: "LAPTOP", name: "Laptops", parent: electronics)

// Define attributes for Laptops
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "RAM_GB", name: "RAM", dataType: DataType.Integer,
    required: true, minValue: 4, maxValue: 128, unit: "GB"
))
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "STORAGE_GB", name: "Storage", dataType: DataType.Integer,
    required: true, unit: "GB"
))
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "SCREEN_SIZE", name: "Screen Size", dataType: DataType.Decimal,
    required: true, unit: "inches"
))

Creating a Product and Instance

// Product (catalog item)
thinkpad = new Product(
    sku: "LEN-X1C-G11-16-512",
    name: "ThinkPad X1 Carbon Gen 11 (16GB/512GB)",
    productType: laptops,
    basePrice: Money(649900, "PLN"),  // 6499.00 PLN
    status: ProductStatus.Draft
)
thinkpad.setAttribute("RAM_GB", 16)
thinkpad.setAttribute("STORAGE_GB", 512)
thinkpad.setAttribute("SCREEN_SIZE", 14.0)

thinkpad.validate()  // isValid: true
thinkpad.publish()   // status = Active

// ProductInstance (specific unit in warehouse)
unit1 = new ProductInstance(
    product: thinkpad,
    serialNumber: "PF4ABC123",
    manufacturedDate: 2025-01-15,
    status: InstanceStatus.Available
)

State Machines

Product Status

create

publish()

discontinue()

delete()

Draft

Active

Discontinued

Deleted

Instance Status

receive into inventory

reserve for order

release reservation

ship to customer

mark damaged

expiry date passed

customer return

restock

return inspection failed

Available

Reserved

Sold

Damaged

Expired

Returned

Invariants

  1. SKU unique — No two products share the same SKU
  2. Serial unique — No two instances share the same serial number
  3. Type hierarchy acyclic — ProductType cannot be its own ancestor
  4. Products on leaf types — Products can only belong to leaf ProductTypes
  5. Required attributes set — Active products must have all required attributes
  6. Valid attribute values — Attribute values must match their definitions

Error Handling

Operation Precondition Violated Error
setAttribute(code, value) Attribute not defined on type PreconditionError: unknown attribute 'FOO' for type Laptop
setAttribute(code, value) Value fails validation PreconditionError: RAM_GB must be between 4 and 128
discontinue() Status is not Active PreconditionError: can only discontinue active products
ProductInstance.sell(order) Not available PreconditionError: instance is not available (status: Sold)
ProductInstance.sell(order) Expired PreconditionError: instance has expired
validate() Missing required attributes Returns ValidationResult(isValid: false, errors: [...]) (no exception)

SQL Schema

sql-- Product type hierarchy
CREATE TABLE product_type (
    id              UUID PRIMARY KEY,
    code            VARCHAR(50) NOT NULL UNIQUE,
    name            VARCHAR(100) NOT NULL,
    description     TEXT,
    parent_id       UUID REFERENCES product_type(id),
    active          BOOLEAN NOT NULL DEFAULT TRUE,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Attribute definitions for product types
CREATE TABLE attribute_definition (
    id              UUID PRIMARY KEY,
    product_type_id UUID NOT NULL REFERENCES product_type(id) ON DELETE CASCADE,
    code            VARCHAR(50) NOT NULL,
    name            VARCHAR(100) NOT NULL,
    data_type       VARCHAR(20) NOT NULL,  -- 'String', 'Integer', 'Decimal', 'Boolean', 'Enum'
    required        BOOLEAN NOT NULL DEFAULT FALSE,
    options         TEXT,  -- JSON array for Enum type: '["S", "M", "L", "XL"]'
    unit            VARCHAR(20),
    min_value       DECIMAL,
    max_value       DECIMAL,
    sort_order      INTEGER NOT NULL DEFAULT 0,
    
    UNIQUE(product_type_id, code)
);

-- Products (catalog items)
CREATE TABLE product (
    id              UUID PRIMARY KEY,
    sku             VARCHAR(50) NOT NULL UNIQUE,
    name            VARCHAR(255) NOT NULL,
    description     TEXT,
    product_type_id UUID NOT NULL REFERENCES product_type(id),
    base_price_cents INTEGER NOT NULL,
    base_price_currency CHAR(3) NOT NULL DEFAULT 'PLN',
    cost_price_cents INTEGER,
    cost_price_currency CHAR(3),
    status          VARCHAR(20) NOT NULL DEFAULT 'Draft',
    weight_kg       DECIMAL(10,3),
    length_cm       DECIMAL(10,2),
    width_cm        DECIMAL(10,2),
    height_cm       DECIMAL(10,2),
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_product_status CHECK (status IN ('Draft', 'Active', 'Discontinued', 'Deleted'))
);

-- Product attribute values (EAV pattern)
CREATE TABLE product_attribute (
    product_id      UUID NOT NULL REFERENCES product(id) ON DELETE CASCADE,
    attribute_code  VARCHAR(50) NOT NULL,
    value_string    VARCHAR(255),
    value_integer   INTEGER,
    value_decimal   DECIMAL(15,4),
    value_boolean   BOOLEAN,
    value_date      DATE,
    
    PRIMARY KEY (product_id, attribute_code)
);

-- Product instances (serialized items)
CREATE TABLE product_instance (
    id              UUID PRIMARY KEY,
    product_id      UUID NOT NULL REFERENCES product(id),
    serial_number   VARCHAR(100) UNIQUE,
    batch_number    VARCHAR(50),
    manufactured_date DATE,
    expiry_date     DATE,
    status          VARCHAR(20) NOT NULL DEFAULT 'Available',
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_instance_status CHECK (
        status IN ('Available', 'Reserved', 'Sold', 'Damaged', 'Expired', 'Returned')
    )
);

-- Indexes
CREATE INDEX idx_product_type ON product(product_type_id);
CREATE INDEX idx_product_status ON product(status);
CREATE INDEX idx_product_type_parent ON product_type(parent_id);
CREATE INDEX idx_instance_product ON product_instance(product_id);
CREATE INDEX idx_instance_status ON product_instance(status);
CREATE INDEX idx_instance_expiry ON product_instance(expiry_date) WHERE expiry_date IS NOT NULL;

Common Queries

sql-- Get product with all attributes
SELECT 
    p.*,
    json_object_agg(pa.attribute_code, 
        COALESCE(pa.value_string, pa.value_integer::text, 
                 pa.value_decimal::text, pa.value_boolean::text, 
                 pa.value_date::text)
    ) as attributes
FROM product p
LEFT JOIN product_attribute pa ON pa.product_id = p.id
WHERE p.id = :productId
GROUP BY p.id;

-- Get products by type (including subtypes)
WITH RECURSIVE type_tree AS (
    SELECT id FROM product_type WHERE id = :typeId
    UNION ALL
    SELECT pt.id FROM product_type pt
    JOIN type_tree tt ON pt.parent_id = tt.id
)
SELECT p.* FROM product p
WHERE p.product_type_id IN (SELECT id FROM type_tree)
  AND p.status = 'Active';

-- Get type hierarchy path
WITH RECURSIVE type_path AS (
    SELECT id, name, parent_id, 1 as level
    FROM product_type WHERE id = :typeId
    UNION ALL
    SELECT pt.id, pt.name, pt.parent_id, tp.level + 1
    FROM product_type pt
    JOIN type_path tp ON pt.id = tp.parent_id
)
SELECT name FROM type_path ORDER BY level DESC;

-- Find available instances with earliest expiry (FEFO)
SELECT pi.* FROM product_instance pi
WHERE pi.product_id = :productId
  AND pi.status = 'Available'
  AND (pi.expiry_date IS NULL OR pi.expiry_date > CURRENT_DATE)
ORDER BY pi.expiry_date NULLS LAST
LIMIT :quantity;

Design Considerations

EAV vs JSON vs Wide Table

Approach Pros Cons
EAV Flexible, sparse attributes Complex queries, no type safety
JSON column Flexible, single column Limited indexing, validation
Wide table Fast, typed, indexable Schema changes, many NULLs

Recommendation:

  • EAV for truly dynamic attributes
  • JSON for semi-structured data
  • Dedicated columns for frequently queried/filtered attributes

When to Use ProductInstance

Scenario Use Instance? Why
Serialized electronics Yes Track individual units
Perishable goods Yes Track expiry dates
Generic commodities (screws) No Just track quantity
Digital products No No physical instance
Made-to-order items Yes Track individual pieces

Pricing Strategies

Product archetype supports various pricing:

  • Base price — On Product entity
  • Customer tier pricing — Calculated at runtime
  • Quantity breaks — Separate PriceBreak entity
  • Time-limited promotions — Promotion entity with dates
  • Order — Products are ordered
  • Inventory — ProductInstance tracked in inventory
  • Party Role (Supplier) — Who supplies products