Progressive Product Catalog Architecture: Building Scalable Commerce from Products to Pricing, Inventory & Offers
As a commerce platform grows, managing products becomes more complicated than creating records with names, images, prices, and stock quantities.
A product might be shared across hundreds of organizations, sold in different variants, listed in multiple catalogs, priced differently by different sellers, stocked at different locations, and subject to several overlapping offers.
The challenge is building an architecture that supports these requirements without tightly coupling everything to a single product model.
This is where Progressive Product Catalog Architecture becomes an interesting design approach.
Instead of forcing every business rule into the initial product schema, the system evolves through clearly separated layers. Each layer adds capabilities while preserving the responsibilities of the existing layers.
The objective is simple:
Build a commerce architecture that can grow from basic product management into a flexible, multi-tenant ecosystem without requiring a complete redesign.
1. Why Do We Need a Progressive Product Catalog Architecture?
Consider two products.
The first is Coca-Cola, a branded product that can be sold by thousands of retailers. Its identity can be shared, but prices, availability, and inventory vary by seller and location.
The second is Chicken Biryani, sold by different restaurants. Two restaurants might use the same product name while offering completely different recipes, portions, ingredients, and prices.
A basic implementation might look like:
Organization
|
└── Products
|
├── Product Information
├── Variants
├── Price
└── StockThis structure might work for a small application, but it becomes restrictive as the business grows.
Should every store create its own Coca-Cola product?
Should a price update at one store affect every other store?
Should two restaurants selling Chicken Biryani share the same product identity?
Should disabling a catalog listing delete the underlying product?
Should changing the current price overwrite the price information associated with older inventory?
These are not simply product-management questions. They represent different concerns with different lifecycles.
A more flexible architecture separates those concerns:
Commerce Platform
|
┌──────────────┼──────────────┐
| | |
Products Categories Attributes
|
▼
Variants
|
▼
Catalog Membership
|
├── Catalog Pricing
└── Store Assignments
|
▼
Store Inventory
|
├── Stock Balances
├── Inventory Lots
└── Stock Movements
|
▼
Pricing & Offer Engine
|
▼
Effective Selling PriceEach layer has a defined responsibility.
The product layer establishes identity. The catalog layer determines how items are offered for sale. Inventory tracks physical availability. Pricing determines the applicable selling price, and the offer engine applies eligible discounts.
The architecture becomes progressively more capable without requiring every product to participate in every advanced feature.
2. Product Identity: Global Products vs Organization-Specific Products
Before designing the catalog, we need to define what a product represents and who owns it.
Not every product belongs to one organization, and not every product should be globally shared.
Example A: Coca-Cola
Suppose the platform already maintains a canonical record for Coca-Cola.
Instead of requiring every retailer to create a duplicate product, the platform can allow organizations to reference the existing identity.
Platform Product
|
└── Coca-Cola
|
├── 300 ml Bottle
├── 500 ml Bottle
├── 1 L Bottle
└── 2 L Bottle
|
┌───────┼───────┐
▼ ▼ ▼
Store A Store B Store CAll three stores can reference the same product variants while independently controlling catalog membership, applicable prices, and inventory.
The product identity remains shared. The selling context does not.
Example B: Chicken Biryani
Now consider two restaurants.
Organization A
└── Chicken Biryani
├── Regular
├── Large
└── Family Pack
Organization B
└── Chicken Biryani
├── Regular
└── SpecialThe names might be identical, but the actual products may differ in recipe, portion size, ingredients, and preparation.
Automatically merging them based on their names would introduce incorrect relationships into the catalog.
A suitable starting model is:
type ProductScope = "PLATFORM" | "ORGANIZATION";
interface Product {
_id: string;
scope: ProductScope;
ownerOrgId: string | null;
canonicalProductId: string | null;
name: string;
brandId: string | null;
categoryId: string;
status: "DRAFT" | "ACTIVE" | "ARCHIVED";
}This is an illustrative TypeScript model rather than a complete production schema.
The important fields are scope, ownerOrgId, and canonicalProductId.
Platform products are managed as shared reference data. Organization-specific products remain independently owned but can optionally reference a canonical product when a valid relationship is established.
This also allows an organization-created product to be linked to a global identity later without requiring the entire catalog to be recreated.
One important distinction remains: canonical identity does not imply universal pricing or universal ownership. A shared product can have different selling prices across different organizations.
3. Progressive Product Configuration: From Simple Products to Rich Variants
A scalable product architecture should not require every product to have the same configuration complexity.
A simple product might need only a name, category, and basic information.
A more complex product might require configurable variants, barcodes, dimensions, packaging, or category-specific attributes.
The system should accommodate both.
Product
|
├── Product Information
├── Brand
├── Category
├── Attributes
└── Variants
|
├── Variant A
├── Variant B
└── Variant CA product describes the underlying item. A variant represents a specific sellable configuration.
For example:
Coca-Cola
|
├── 300 ml Bottle
├── 500 ml Bottle
├── 1 L Bottle
└── 2 L BottleFor a restaurant, variants might represent Regular, Large, and Family Pack portions.
A variant model could look like:
interface ProductVariant {
_id: string;
productId: string;
name: string;
sku: string;
barcode?: string;
attributes: Record<string, unknown>;
status: "ACTIVE" | "INACTIVE";
}A production implementation should validate attributes against the product's category and attribute definitions rather than accepting arbitrary values without constraints.
More importantly, variants should not contain every organization's prices or every store's stock.
Those concerns belong to separate layers.
Why progressive configuration matters
A platform may start by supporting basic product information and a few manually created variants.
Later, it can introduce:
Dynamic attribute definitions.
Bulk variant generation.
Barcode-based identification.
Category-specific validation.
Vendor product imports.
Assisted product matching.
These features extend the configuration workflow without changing the fundamental meaning of a product or variant.
4. Categories and Attributes: Shared Definitions, Flexible Extensions
Categories and attributes provide a consistent way to organize and describe products.
However, a scalable platform should not require every organization to use exactly the same taxonomy or attribute set.
Category architecture
A platform-managed category hierarchy might look like:
Food & Beverages
|
├── Beverages
| |
| └── Soft Drinks
| └── Cola
|
└── Prepared Food
|
└── BiryaniOrganizations can use platform categories while defining additional subcategories where permitted.
For example:
Platform Category
└── Prepared Food
|
└── Organization Extension
└── House Special BiryaniA category model could include:
interface Category {
_id: string;
scope: "PLATFORM" | "ORGANIZATION";
ownerOrgId: string | null;
name: string;
parentId: string | null;
canonicalCategoryId: string | null;
status: "ACTIVE" | "INACTIVE";
}An organization-specific category can optionally reference a canonical category.
The platform should avoid copying the entire global category tree for every organization. Shared categories should remain shared, while organization-specific extensions retain their own ownership.
Attribute architecture
Different categories require different attributes.
A beverage might use Volume and Pack Type. Clothing might use Size and Color. Restaurant food might use Portion Size and Spice Level.
Instead of hardcoding every attribute permanently, define attributes as reusable metadata.
interface AttributeDefinition {
_id: string;
name: string;
scope: "PLATFORM" | "ORGANIZATION";
ownerOrgId: string | null;
appliesTo: "PRODUCT" | "VARIANT";
dataType: "STRING" | "NUMBER" | "BOOLEAN" | "OPTION";
isVariantAxis: boolean;
allowedCategoryIds: string[];
allowedValues?: string[];
}These fields are illustrative; additional validation rules, units, and localization can be added as needed.
The critical distinction is between an attribute that describes a product and an attribute that generates variants.
For example, a water bottle may use Capacity and Material as descriptive or variant-defining attributes. A restaurant might use Spice Level as an order modifier rather than creating a separate permanent SKU for every possible spice preference.
Not every attribute should produce a new variant.
A flexible attribute system describes products without forcing every possible configuration into the product catalog.
5. The Catalog Layer: Separating Product Identity from Selling Context
A catalog is an assortment of products and variants offered within a particular commercial context.
It is not another product master.
Consider two retailers:
Catalog A: FreshMart Retail
|
├── Coca-Cola 500 ml
├── Coca-Cola 1 L
├── Mineral Water 1 L
└── Bread 400 gCatalog B: Green Basket
|
├── Coca-Cola 500 ml
├── Organic Milk 1 L
├── Whole Wheat Bread
└── Fresh VegetablesBoth catalogs can reference the same Coca-Cola variant.
However, each retailer independently determines whether it offers that variant and which pricing configuration applies.
A basic catalog model:
interface Catalog {
_id: string;
organizationId: string;
name: string;
description?: string;
status: "ACTIVE" | "INACTIVE";
}Catalog membership should be represented separately:
interface CatalogItem {
_id: string;
catalogId: string;
variantId: string;
status: "ACTIVE" | "INACTIVE";
pricingMode: "INHERIT" | "OVERRIDE";
priceEntryId?: string;
overridePrice?: number;
}The relationship becomes:
Product
|
▼
Product Variant
|
├──────────────────┐
▼ ▼
Catalog Item A Catalog Item B
| |
▼ ▼
Catalog A Catalog BThe catalog item references existing product data rather than duplicating the entire product.
This separation allows an organization to deactivate a listing without deactivating the underlying product for other organizations.
It also allows product information to evolve independently of the catalog's availability and pricing decisions.
A useful design rule
Product identity should be reusable; catalog membership should be independently manageable.
This is one of the foundational boundaries in a multi-tenant commerce architecture.
6. Pricing Is a Progressive Layer, Not a Single Product Field
A flexible catalog system must support more than one source of pricing.
A variant has a default price in the relevant seller or pricing context. A catalog may inherit that price or override it. A specific inventory lot may also require exceptional pricing treatment.
These are distinct stages, not independent values that should simply be added together.
Variant-level pricing
Consider a restaurant offering three biryani sizes:
Chicken Biryani
|
├── Regular ₹200
├── Large ₹280
└── Family Pack ₹370Each variant has a different selling price.
For a shared branded product, the canonical product identity does not automatically determine the price that every seller must charge.
The applicable variant price should belong to the appropriate seller or pricing context.
An illustrative price model:
interface VariantPrice {
_id: string;
variantId: string;
organizationId: string;
sellingPrice: number;
mrp?: number;
currency: string;
effectiveFrom: Date;
effectiveTo?: Date;
}This model represents time-bounded pricing. A production implementation should also define monetary precision, uniqueness, and price-history rules.
Catalog-level pricing
When adding a variant to a catalog, the organization can inherit the applicable variant price or override it.
Variant Price: ₹200
|
├── INHERIT → ₹200
|
└── OVERRIDE → ₹190The catalog override applies to that listing, not to every catalog using the same variant.
The canonical product remains unchanged.
Inventory-level pricing
There is another situation that deserves attention.
Suppose the price of Coca-Cola 1 L increases from ₹80 to ₹85, but the store still has stock received under the earlier price.
The inventory system may need to retain the original batch's pricing information, including acquisition cost and the marked retail price where applicable.
Current Variant Price: ₹85
|
▼
Store Inventory
|
┌─────┴─────┐
▼ ▼
Older Lot New Lot
| |
Price data Price data
preserved preservedPreserving old batch data does not necessarily mean selling old stock at its previous selling price.
The business must define whether older stock continues at the current price or qualifies for a specific lot-level price override.
These cases require separate fields for:
MRP: The marked retail price, where applicable.
Cost price: The acquisition cost of the stock.
Selling price: The price charged before applicable discounts.
An inventory lot can preserve relevant price snapshots and support an authorized, time-bounded selling-price override when necessary.
The lot-level rule should only apply when the actual lot can be identified during order allocation and fulfillment.
Otherwise, the application risks displaying a price that does not match the stock eventually supplied.
The complete pricing flow
Variant Price
|
▼
Catalog Pricing Rule
|
▼
Inventory-Lot Price Rule
(if applicable)
|
▼
Eligible Offers & Discounts
|
▼
Final Selling PriceThe first layers resolve the relevant selling price. The offer engine then applies eligible discounts according to the platform's pricing policy.
Keep price ownership, historical price data, and discount eligibility separate. They solve different problems.
7. Store Inventory: Availability Belongs to the Location
A product can exist globally, belong to an active catalog, and still be unavailable at a particular store.
This is why inventory should not live directly inside the product or catalog item.
Consider a retailer with three branches:
Organization
|
├── Store A
| ├── Coca-Cola 1 L: 40 bottles
| └── Water 1 L: 20 bottles
|
├── Store B
| ├── Coca-Cola 1 L: 0 bottles
| └── Water 1 L: 35 bottles
|
└── Store C
├── Coca-Cola 1 L: 75 bottles
└── Water 1 L: 10 bottlesThe underlying product variant can be shared across all three stores.
However, each store has its own inventory state.
The core entities might include:
EntityResponsibilityOrganizationBusiness or tenant ownershipStorePhysical branch and operational locationInventory balanceCurrent stock for a variant at a storeInventory lotBatch-specific quantity, expiry, and cost informationStock movementAuditable stock changesStore-catalog assignmentDetermines which assortment a store can use
The resulting relationship:
Organization
|
├── Store A
├── Store B
└── Store C
|
▼
Inventory Balance
|
▼
Inventory Lots
|
▼
Stock MovementsThe inventory balance represents current stock. Inventory lots represent individual batches, while stock movements provide an audit trail of receipts, sales, returns, transfers, and adjustments.
A store may use a shared catalog while maintaining independent stock.
Likewise, an inventory record may remain necessary for auditing even after a catalog item has been deactivated.
What about prepared food?
Not every business manages finished products in the same way.
A retailer may track bottles, packages, and individual items. A restaurant may eventually require ingredient inventory and recipe-based consumption.
Both can use the same high-level principle: inventory belongs to the relevant organization and location, but the operational details depend on the product and business model.
The foundation should remain extensible without pretending that every product category requires identical inventory handling.
8. Offers and Discounts: Multiple Scopes, One Pricing Engine
Once product, catalog, and inventory pricing are separated, offers can become their own layer.
A commerce platform may support offers at several scopes:
Offer Engine
|
├── Platform Offers
|
├── Organization Offers
|
├── Location Offers
|
├── Store Offers
|
└── Inventory-Specific MarkdownsEach scope serves a different purpose.
ScopeExamplePlatformA promotion available across participating sellersOrganizationA retailer's festival discountLocationA promotion restricted to a particular regionStoreA branch-specific campaignInventory-specificA markdown for eligible stock approaching expiry
The important part is that offers should not directly modify product or catalog records.
They should be evaluated by a central pricing resolver.
Conceptually:
Resolved Selling Price
|
▼
Find Eligible Promotions
|
▼
Apply Precedence and Stacking Rules
|
▼
Validate Discount Limits
|
▼
Final Selling PriceNot every eligible promotion should automatically stack with every other promotion.
The pricing engine must define precedence, mutual exclusions, discount limits, and the conditions under which a more specific offer overrides a broader one.
Example: Near-expiry stock
Suppose a store has 20 bottles from an older inventory batch and 100 bottles from a newer batch.
The store may want to offer a clearance discount only on the older stock.
Store Inventory
|
├── Older Batch
| └── Eligible for Clearance
|
└── Newer Batch
└── Standard PricingA stock-specific markdown must target the eligible lot rather than accidentally discounting the entire product.
This requires the inventory and pricing systems to work together while retaining independent responsibilities.
For completed orders, the platform should retain the resolved selling price, applied discounts, and relevant pricing-rule information so later changes do not rewrite historical transactions.
9. Catalog Processing and Confidence-Based Product Matching
A scalable catalog architecture eventually needs a reliable way to process products from vendors, external catalogs, spreadsheets, and other data sources.
Automatically creating or merging products based only on their names is unsafe.
For example, several restaurants may offer Chicken Biryani. A simple text match cannot establish that they sell the same product.
Likewise, two products with similar names may have different brands, packaging, sizes, or identifiers.
A processing workflow should look like:
External Product Data
|
▼
Normalize and Validate
|
▼
Generate Match Candidates
|
▼
Evaluate Confidence and Conflicts
|
├── Strong Match
| └── Apply Safe Matching Rules
|
├── Uncertain Match
| └── Human Review
|
└── No Valid Match
└── Create or Retain Local ProductThe confidence calculation may consider barcode or GTIN, manufacturer, brand, category, packaging, and attribute similarity.
An illustrative decision record:
interface ProductMatch {
sourceProductId: string;
candidateProductId: string;
confidenceScore: number;
matchMethod: string;
evidence: Record<string, unknown>;
status:
| "SUGGESTED"
| "CONFIRMED"
| "REJECTED"
| "NEEDS_REVIEW";
}A confidence score belongs to a specific matching decision, together with the evidence and matching method. It is not a permanent property of the canonical product.
An uncertain match can be reviewed, rejected, or reconsidered when better evidence becomes available.
Even a strong identifier match should be checked for data conflicts before automatically linking products.
The platform can progressively introduce better normalization, review tools, automated matching, and assisted catalog enrichment without changing its core product and catalog boundaries.
10. Important Data Integrity Rules
The flexibility of this architecture depends on enforcing clear relationships.
Avoid duplicate catalog items
Within the intended catalog scope, the same variant should not accidentally appear multiple times.
A common constraint is:
Unique:
catalogId + variantIdIf the business needs the same variant to appear under different selling conditions, those conditions should be modeled explicitly.
Keep tenant boundaries explicit
Every organization-owned product and catalog should have clear ownership.
Store assignments must not allow one organization to access another organization's private catalog without authorization.
Avoid uncontrolled cascading deletion
Deactivating a catalog item should not delete its product, variant, inventory history, or completed orders.
Use explicit lifecycle states and retention rules.
Preserve pricing history
Price changes should be auditable. Inventory-lot snapshots should preserve the information needed for the relevant stock and compliance workflows.
Completed orders should retain their own price and discount snapshots.
Separate listing status from stock status
An active catalog item may be out of stock at one branch but available at another.
An inactive listing may still have inventory records that must be retained for reconciliation or auditing.
These are separate states and should not be represented by a single isAvailable field.
Do not introduce every advanced model immediately
Clear domain boundaries are important from the beginning, but a basic commerce platform does not need a complicated rules engine, extensive matching automation, or advanced stock markdowns on its first day.
The model should support progression while the implementation grows according to actual business needs.
11. Building the Architecture Progressively
A useful implementation strategy is to introduce the foundational relationships first and progressively add more sophisticated capabilities.
Phase 1: Product Foundation
|
├── Global and Organization Products
├── Categories and Attributes
└── Product Variants
|
▼
Phase 2: Catalog Management
|
├── Catalog Creation
├── Catalog Items
├── Store Assignments
└── Inherited and Override Pricing
|
▼
Phase 3: Inventory
|
├── Store Stock Balances
├── Stock Movements
├── Inventory Lots
└── Expiry Tracking
|
▼
Phase 4: Pricing and Offers
|
├── Central Price Resolution
├── Organization and Platform Offers
├── Location and Store Promotions
└── Inventory-Specific Markdown
|
▼
Phase 5: Advanced Catalog Processing
|
├── Product Imports
├── Canonical Matching
├── Confidence-Based Review
└── Assisted Catalog EnrichmentThis approach allows the platform to deliver useful functionality early while preserving the ability to expand into more complex business requirements.
The key is to establish the right boundaries before adding more features.
A modular implementation does not automatically require microservices. These boundaries can initially exist as clear domain modules within a well-structured application and be separated into services later when operational needs justify doing so.
12. The Bigger Picture
A catalog is not simply a list of products.
It is the connection between product identity and the context in which an organization sells an item.
Without clear boundaries, the product model gradually becomes responsible for everything:
Product
|
├── Product Information
├── Organization Pricing
├── Catalog Membership
├── Store Stock
├── Inventory-Lot Pricing
└── PromotionsThis makes independent changes difficult and creates unnecessary dependencies.
A progressive architecture separates those responsibilities:
Product Identity
|
▼
Product Variants
|
▼
Catalog Membership
|
├── Catalog Pricing
|
▼
Store Inventory
|
├── Inventory Lots
└── Stock Movements
|
▼
Pricing & Offer Engine
|
▼
Effective Selling PriceEach layer can evolve according to its own requirements.
Products can be shared without forcing common selling prices.
Catalogs can change without duplicating product information.
Organizations can maintain their own product identities and commercial rules.
Stores can manage inventory independently.
Offers can grow more sophisticated without modifying the underlying product records.
New categories, product types, and matching capabilities can be introduced without rebuilding the foundation.
The architecture progresses as requirements grow, rather than becoming more complicated merely for the sake of flexibility.
A scalable product catalog is not one where everything is configurable from day one. It is one where each layer has a clear responsibility, each relationship is explicit, and new capabilities can be added without destabilizing the existing system.