Skip to main content

Database Design & Polymorphic Catalog

The scalable NoSQL data modeling strategy for the diverse Reshma-Core inventory.

MongoDB Mongoose


1. The Polymorphic Strategy (Single Collection)

In a traditional SQL database, handling completely different item types (like Glass Bangles versus Unstitched Fabrics) requires complex JOIN tables or the Entity-Attribute-Value pattern. Both approaches severely degrade read performance as the catalog grows.

Reshma-Core utilizes Mongoose Discriminators, a polymorphic NoSQL pattern. All products, regardless of category, live inside one single products collection. This allows global text searches, universal pagination, and unified category filtering to operate in milliseconds.

At the application layer, Mongoose enforces unique validation schemas based on the itemType discriminator key. The database will physically reject a query if an Apparel item attempts to save a Bangle‑specific property.


1.1 Architecture Decision Record: Why Polymorphism?

Before settling on Mongoose Discriminators, we evaluated two other common e‑commerce database patterns.

Rejected Alternative 1: Multiple Collections

  • Idea: Create a separate collection for each category (Apparel, Bangles, Fabrics).
  • Why it fails: A customer search for the colour "Red" would require querying multiple collections, merging pagination in Node.js memory – slow and non‑scalable.

Rejected Alternative 2: The EAV Pattern (Entity‑Attribute‑Value)

  • Idea: Store custom fields in a generic array (attributes: [{ key: 'size', value: 'XL' }]).
  • Why it fails: No type safety; the database cannot prevent storing a bangle size on a saree. Indexing dynamic keys is inefficient.

The Winning Solution: Single Collection Polymorphism

  • How it works: All items live in one collection, discriminated by itemType. Mongoose applies the correct sub‑schema at the application layer.
  • Benefits: Lightning‑fast global searches, strict per‑type validation, easy addition of new types without schema changes.

2. Base Product Schema

Every product – regardless of type – includes these fields. They are defined in base-product.model.ts and base-product.interface.ts.

FieldTypeRulesDescription
itemTypeString (enum)RequiredDiscriminator key: "BANGLE", "APPAREL", "FABRIC", "INNERWEAR", "ACCESSORY"
skuStringRequired, unique, uppercase, trimmedStock Keeping Unit (warehouse barcode).
nameStringRequired, trimmedDisplay title.
mainCategoryString (enum)RequiredOne of: Sarees, Apparel, Accessories, Innerwear, Bangles
subCategoryStringRequired, trimmede.g., “Handloom”, “Kurti”, “Glass Bangles”
materialStringRequired, trimmedMaterial composition.
sellingUnitString (enum)RequiredSingle Piece, Meter, Set, Pair, Dozen, Pack
colorsString[]Optional, default []Array of colour names.
basePriceNumberRequired, min 0Raw price before discount and taxes.
discountNumberDefault 0, min 0, max 100Percentage discount.
currentStockNumberRequired, min 0Current inventory level.
weightGramsNumberRequired, min 0Physical weight – used for shipping calculations.
isFragileBooleanRequiredIf true, returns require photographic evidence.
imagesString[]Required, min 1Cloudinary URLs (auto‑cleaned on delete).
hsnCodeStringRequired, 4‑8 digitsHSN code for Indian GST.
taxProfileString (enum)RequiredOne of the TaxProfile values (e.g., IMITATION_JEWELLERY, STITCHED_APPAREL).
ratingsMetadataObjectAuto‑managedAggregated reviews: averageRating, totalReviews, ratingDistribution (1‑5 stars).
tagsString[]Optional, default []Search keywords.
isActiveBooleanDefault trueSoft‑delete flag – never hard‑delete products to preserve historical orders.
createdAt / updatedAtDateAutoTimestamps.

Note: The description field is not part of the base interface (it is present in the seeding script but not enforced in the model). Product descriptions are handled via a separate details field or not stored in the database.


3. Discriminator Sub‑Schemas

Each product type adds its own fields. These are defined in separate model files (e.g., bangle.model.ts, apparel.model.ts) and inherit the base schema.

3.1 Bangle Schema (itemType: "BANGLE")

Used for: Glass, metal, lac bangles.

FieldTypeRulesNotes
bangleSizesString[]Required, at least one, enum: "2.2", "2.4", "2.6", "2.8"Traditional Indian bangle diameters.
packSizeNumberDefault 12, min 1Often sold by the dozen.

Example (from bangle.model.ts):

bangleSizes: { type: [String], required: true, enum: ["2.2","2.4","2.6","2.8"] },
packSize: { type: Number, default: 12, min: 1 }

3.2 Apparel Schema (itemType: "APPAREL")

Used for: Sarees, kurtis, suits, lehengas, readymade garments.

FieldTypeRulesNotes
sizesString[]Required, at least one, enum: "XS", "S", "M", "L", "XL", "XXL", "Free Size", "34", "36", "38", "40"Supports both letter and numeric sizes.
customTailoringBooleanDefault falseIf true, checkout collects custom measurements.
careInstructionsStringOptional, trimmedWash care details.

3.3 Fabric Schema (itemType: "FABRIC")

Used for: Unstitched fabric sold by length.

FieldTypeRulesNotes
lengthMetersNumberRequired, min 0.1Length in metres.
customTailoringBooleanDefault trueUnstitched fabric usually requires tailoring.

3.4 Innerwear Schema (itemType: "INNERWEAR")

Used for: Bras, panties, shapewear.
Hygiene policy: Non‑returnable – enforced at schema level.

FieldTypeRulesNotes
cupSizesString[]Required, at least one, enum: "32B", "34B", "36C", "34C", "36D"Common Indian bra sizes.
isReturnableBooleanForced false (set: () => false)Cannot be overridden.

3.5 Accessory Schema (itemType: "ACCESSORY")

Used for: Jewellery, bags, bindis, hair accessories.

FieldTypeRulesNotes
sizeDetailsStringRequired, trimmedFree‑text size description (e.g., “Adjustable”, “One Size”).

4. Indexing & Performance Strategy

To ensure the catalog scales to tens of thousands of items, the following indexes are created at the MongoDB layer (see base-product.model.ts):

// Text search index – global search bar (name, tags, subCategory)
BaseProductSchema.index({ name: "text", tags: "text", subCategory: "text" });

// Compound index for filtered listing – active → type → category → newest
BaseProductSchema.index({
isActive: 1,
itemType: 1,
mainCategory: 1,
createdAt: -1,
});

// Unique index on SKU – prevents duplicate warehouse identifiers
BaseProductSchema.index({ sku: 1 }, { unique: true });
  • The text index enables fast keyword searches without an external search engine (though Typesense is also used for typo‑tolerant search).
  • The compound index covers common queries like “show all active bangles, newest first” and prevents full collection scans.
  • The unique index on SKU acts as a database‑level firewall.

5. Automatic Cloudinary Cleanup

When a product is hard‑deleted (using findOneAndDelete), a Mongoose pre('findOneAndDelete') hook runs. It retrieves the document, extracts the images array, and calls deleteFromCloudinary() for each URL. This prevents orphaned images from incurring costs.

Soft deletion (isActive = false) does not delete images – the product remains in the database for historical order integrity.


6. Zod Validation (Admin DTO)

The CreateProductSchema in product.admin.dto.ts uses a Zod discriminated union to enforce the correct fields based on itemType. For example:

const BangleSchema = BaseProductSchema.extend({
itemType: z.literal("BANGLE"),
bangleSizes: z.array(z.enum(["2.2","2.4","2.6","2.8"])).min(1),
packSize: z.number().int().min(1).default(12),
});

If an admin tries to create a bangle with cupSizes (an innerwear field), Zod rejects the request with 400 Bad Request. This is the first firewall before the database.


FilePurpose
src/modules/products/interfaces/base-product.interface.tsTypeScript interface for base product.
src/modules/products/models/base-product.model.tsMongoose schema, indexes, pre‑delete hook.
src/modules/products/interfaces/*.tsDiscriminator interfaces.
src/modules/products/models/*.model.tsDiscriminator models.
src/modules/products/dtos/product.admin.dto.tsZod validation for admin creation/update.
src/modules/orders/tax.utils.tsTaxProfile enum.

Next Steps


The Reshma-Core Team