Catalog Product Backend Documentation
Backend architecture and runtime contracts for catalog product flows.
Catalog Product - Backend Documentation
1. Backend Scope and Boundaries
The product backend owns catalog product lifecycle, slug continuity, product-tag linking, and customer product reads. It does not own checkout/order payment orchestration, but it provides pricing and sellability source-of-truth used by those modules.
2. Module Composition (Aggregate + Leaf)
- Aggregate admin composition:
apps/api/src/modules/catalog/catalog-admin.module.ts- Includes
ProductAdminModule
- Includes
- Leaf admin module:
apps/api/src/modules/catalog/admin/product/product-admin.module.ts - Leaf customer module:
apps/api/src/modules/catalog/customer/product/product-customer.module.ts - Mobile composition:
apps/api/src/modules/mobile/mobile.module.tsmounts customer module under/mobile.
3. Data Model (Drizzle / PostgreSQL)
Primary tables and joins:
product(packages/db/src/schema/product/product.ts)product_slug_history(packages/db/src/schema/product/product-slug-history.ts)product_tagjoin (packages/db/src/schema/product/product-tag.ts)- Related lookup tables:
category,tag,featured_product,product_type
Key constraints/indexes used by runtime:
product.sluguniqueproduct.product_type_id— FK toproduct_type.id(indexed viaproduct_type_id_idx)product.product_kindenum:physical | digital | care_package(nullable; existing rows default to NULL, treated as non-physical)product_kind_idxindex onproduct.product_kindproduct.brand_id— nullable FK tobrand.id(indexed viabrand_id_idx)product_tagcomposite primary key (product_id,tag_id)- money columns
mrpandspare stored as integer minor units (for Stripe, USD cents) - trigram indexes on
product.titleandproduct.description
IT Mart product columns on product:
product_kind(enumproduct_kind, nullable) —physical | digital | care_packageproduct_type_id(integer, FK toproduct_type.id, required) — references product type reference tablespecifications(jsonb, nullable) — flexible per-product-type specs objectis_best_seller(boolean, required, default false) — storefront merchandising flag
Additional columns added in migration 0003_brand_series_and_product_extensions:
| Column | Type | Default | Notes |
|---|---|---|---|
is_trending | boolean | false | Admin-set trending flag |
trending_expires_at | timestamptz | null | null = infinite; future date = expires then |
is_best_seller_expires_at | timestamptz | null | null = infinite; future date = popular until then |
series_id | integer | null | FK to brand_series.id, ON DELETE SET NULL |
Index additions:
product_trending_idxonproduct.is_trendingproduct_trending_expires_idxonproduct.trending_expires_atproduct_series_id_idxonproduct.series_id
Dropped columns (removed in migration 0001_itmart_product_foundation):
delivery,materials,certificate,is_master_grade,size,age_disclaimer,course_id
4. Runtime Rules and Domain Invariants
spcannot exceedmrp.productKindis required on create; validated asphysical | digital | care_package.productTypeIdis required on create; must reference an existingproduct_type.id.specificationsis optional; stored as-is in JSONB without schema validation.- Physical check uses
isPhysicalProductKind(productKind)— returnstrueonly forphysical. - Non-physical (digital, care_package) and null
productKindrows return always-available in availability endpoint. - Status transitions are constrained:
draft -> publishedpublished -> unlistedunlisted -> archived
- Customer list/detail include only
publishedandisSellable=trueproducts. - Slug uniqueness check includes both current products and slug-history rows.
- Multi-table writes (product + slug history + product_tag) are transaction-wrapped.
isTrendingis admin-set;trendingExpiresAtis computed fromtrendingDaysparam at create/update time.isBestSeller(existing) gains an expiry columnisBestSellerExpiresAt; computed frompopularDaysat create/update.- Expiry check logic:
or(isNull(expiresAt), gt(expiresAt, NOW()))— both boolean AND expiry must pass for a product to appear in discovery filters. - Setting a flag to
false(e.g.,isTrending=false) nullifies the expiry column. averageRatingandreviewCountare computed via scalar subqueries in the SELECT clause on every list query. Onlystatus='approved'reviews are counted.minRatingfilter uses a correlated AVG subquery in the WHERE clause. Only applied whenminRatingparam is present.parentCategoryIdfilter uses an IN subquery:category WHERE id=$parent OR parent_id=$parent(2-level only; no CTE).seriesIdfilter uses direct FK equality:eq(products.seriesId, brandSeriesId).
5. Caching Strategy
Customer read cache keyspaces:
catalog:products:list:catalog:product:slug:catalog:products:featured:
All keys use CacheKeyUtil.build(...) with deterministic segments.
Additional discovery cache keyspaces:
catalog:products:new-arrivals:
catalog:products:best-sellers:
catalog:products:popular:
catalog:products:trending:All new filter params (brandId, minPrice, maxPrice, isBestSeller, minRating, parentCategoryId, brandSeriesId, isTrending) are included in buildCacheKey() — each unique filter combination produces its own cache entry.
Admin write invalidation uses prefix patterns via invalidatePattern(...) for all above keyspaces.
5.1 Discovery Query Behavior
Customer product discovery supports filtering by brand and brand-series parameters:
brandId— exact match filter onproduct.brand_idbrandSlug— resolves brand by slug, then filters products by the resolved brand IDseriesId— exact match filter onproduct.series_id- These filters can be combined with existing
categoryId,productTypeId, andtagIdfilters - Brand-series discovery (public) returns only visible series (
isVisible=true) - Brand-based filtering also respects the brand's visibility flag
5.1.1 Customer discovery flow
brand -> brand-series (optional) -> products filtered by series/brandThe discovery chain flows from brand selection → optional series drill-down → product list. Each step narrows the product result set:
- Brand-scoped:
WHERE product.brand_id = :brandId - Series-scoped:
WHERE product.series_id = :seriesId - Both:
WHERE product.brand_id = :brandId AND product.series_id = :seriesId
6. Rate Limiting Strategy
Redis sorted-set sliding-window limiter is implemented in controllers for list/search endpoints:
- Admin list route limiter key prefix:
rl:catalog:admin:products:search: - Customer list route limiter key prefix:
rl:catalog:customer:products:search: - Exceed path throws
RateLimitExceededException.
7. Error and Resilience Contracts
Primary error codes:
PRODUCT_NOT_FOUNDPRODUCT_CREATE_FAILEDPRODUCT_CATEGORY_NOT_FOUNDPRODUCT_TAG_NOT_FOUNDPRODUCT_SP_EXCEEDS_MRPPRODUCT_INVALID_STATUS_TRANSITIONPRODUCT_INVALID_SORTPRODUCT_BRAND_NOT_FOUNDPRODUCT_SERIES_NOT_FOUNDBRAND_SERIES_NOT_FOUNDRATE_LIMIT_EXCEEDED
Redis read/write/cache-invalidation failures are logged and gracefully fall back to DB reads where implemented.
8. Performance Notes
- List queries use projection selects instead of loading full rows.
- Search combines trigram similarity + ILIKE with escaped patterns.
- Pagination uses
PaginationUtil.getDrizzleParams(...). - Count queries are executed in parallel with list query on customer list paths.
9. Backend Diagram
10. Release/QA Checklist
- Validate CRUD and permission gates for
Products_*. - Validate slug-history insertion and old-slug resolution.
- Validate rate-limiter thresholds through env values.
- Validate cache invalidation for create/update/delete.
- Validate transaction integrity for product + tags + slug-history updates.
11. File Map
| Concern | File Path | Purpose |
|---|---|---|
| Admin product | apps/api/src/modules/catalog/admin/product/* | Product CRUD |
| Customer product | apps/api/src/modules/catalog/customer/product/* | Product discovery |
| Admin brand-series | apps/api/src/modules/catalog/admin/brand/brand-series/* | Brand series CRUD |
| Customer brand-series | apps/api/src/modules/catalog/customer/brand/brand-series/* | Brand series reads |
| DB schema | packages/db/src/schema/product/* | Table definitions |
| DB product_type schema | packages/db/src/schema/product/product-type.ts | product_type reference table |
| DB brand_series schema | packages/db/src/schema/catalog/brand-series.ts | brand_series table |
| Admin product type | apps/api/src/modules/catalog/admin/product-type/* | Product type CRUD |
| Migration 0003 | packages/db/src/migrations/0003_brand_series_and_product_extensions.sql | Schema changes |
12. Environment Variables
| Variable | Default | Description |
|---|---|---|
SEARCH_SIMILARITY_THRESHOLD | 0.3 | Minimum similarity score for search results |
See Also
- Feature Guide: See Catalog Product - Feature List Section 6 (State Models) for product lifecycle diagrams and status transitions.
- API Reference: See Catalog Product - API & Integration Guide Section 7 (Endpoint Reference + Payload Cheatsheet) for API contracts.
- Product Type Module: See Product Type - Backend Documentation for product type reference table backend.
- Product Type API: See Product Type - API & Integration Guide for product type API contracts.