Shop It Docs
Developer ResourcescatalogBrand Series

Brand Series Module Backend Documentation

Backend architecture, data model, invariants, and caching for catalog brand-series flows.

Brand Series Module - Backend Documentation

1. Backend Scope and Boundaries

Brand Series backend owns:

  • brand series CRUD operations (admin surface)
  • visible-only listing and slug-based detail (customer/mobile surface)
  • auto-slug generation from name with suffix resolution on conflict
  • brand and category existence validation for write operations
  • read-side caching for customer-facing endpoints
  • reorder operation for storefront display sequencing

Brand Series backend does NOT own:

  • brand or category management (owned by BrandModule and CategoryModule respectively)
  • product-to-series linkage (products have a nullable series_id FK, but product write operations are owned by ProductModule)
  • queue/worker processing (all operations are synchronous)
  • outbox event dispatching (no async side effects)
  • rate limiting (no module-level throttling)
  • status history or audit log (state changes are timestamp-only, no append-only history table)

2. Module Composition (Aggregate + Leaf)

BrandSeriesAdminModule composes:

  • BrandSeriesAdminController -- HTTP surface for admin CRUD
  • BrandSeriesAdminService -- business logic for admin operations

BrandSeriesCustomerModule composes:

  • BrandSeriesCustomerController -- HTTP surface for public listing and detail
  • BrandSeriesCustomerService -- business logic with caching layer

Ownership model:

  • admin module is a leaf module imported by BrandAdminModule which is composed into CatalogAdminModule
  • customer module is a leaf module imported by BrandCustomerModule which is composed into CatalogCustomerModule
  • both modules share no service; they operate independently with their own DB and cache dependencies
  • mobile surface is composed by MobileModule which re-exports the customer module routes under /api/mobile/

3. Data Model (Drizzle / PostgreSQL)

3.1 Primary table: brand_series

ColumnTypeConstraintsNotes
idserialPKAuto-increment primary key
brand_idintegerNOT NULL, FK -> brands.id ON DELETE CASCADEOwning brand
category_idintegerNOT NULL, FK -> categories.id ON DELETE CASCADEProduct category for this series
namevarchar(255)NOT NULLDisplay name
slugvarchar(255)NOT NULL, UNIQUEURL-safe identifier; auto-generated
descriptiontextNULLABLEOptional description
image_urlvarchar(500)NULLABLEOptional image URL
is_activebooleanNOT NULL, DEFAULT trueVisibility for customer endpoints
orderintegerNOT NULL, DEFAULT 0Display order (lower = first)
created_attimestamp with time zoneNOT NULL, DEFAULT now()Creation timestamp
updated_attimestamp with time zoneNOT NULL, DEFAULT now()Last update timestamp; auto-updated via $onUpdateFn

3.2 Foreign keys

  • brand_id references brands.id with ON DELETE CASCADE -- deleting a brand removes all its series
  • category_id references categories.id with ON DELETE CASCADE -- deleting a category removes all its series
  • Products link to series via product.series_id (nullable FK to brand_series.id) -- this FK is defined in the product schema, not in brand-series

3.3 Indexes

Index nameTypeColumnsPurpose
brand_series_pkeyPKidPrimary key lookup
brand_series_slug_uidxUNIQUEslugSlug uniqueness for customer detail lookup
brand_series_brand_category_uidxUNIQUEbrand_id, category_idEnforces one series per (brand, category) pair
brand_series_brand_id_idxINDEXbrand_idFilter by brand in list queries
brand_series_category_id_idxINDEXcategory_idFilter by category in list queries
brand_series_active_order_idxINDEXis_active, orderCustomer list query: visible series sorted by order

3.4 Schema source of truth

  • packages/db/src/schema/catalog/brand-series.ts
  • product -- has series_id (nullable FK to brand_series.id), used by BrandSeriesCustomerService to compute productCount
  • brand -- FK target for brand_id
  • category -- FK target for category_id

4. Runtime Rules and Domain Invariants

4.1 Name uniqueness within brand-category

Series name does not have a standalone unique constraint. Instead, the unique constraint is on the (brand_id, category_id) pair. This means:

  • There cannot be two series for the same brand and category combination.
  • Different brands can have series with the same name.
  • The same brand can have series with the same name if they are assigned to different categories.
  • Duplicate attempt returns BRAND_SERIES_DUPLICATE (HTTP 409).

4.2 Slug generation

Slug is auto-generated from name using the following pipeline:

  1. Trim whitespace, convert to lowercase.
  2. Remove all non-alphanumeric characters except hyphens and spaces.
  3. Replace spaces with hyphens.
  4. Collapse consecutive hyphens.
  5. Strip leading/trailing hyphens.
  6. If result is empty, use "series" as the base.
  7. Check for slug conflicts in DB:
    • If no conflict, use the base slug.
    • If conflict exists with a different series id, append -1, -2, etc. until a unique slug is found.
    • Slug update during edit skips the current series' own slug.

4.3 Visibility rules

  • Customer/mobile endpoints always apply WHERE is_active = true filter.
  • Admin endpoints return all series regardless of isActive.
  • Products can be linked to invisible series; the visibility of the series does not affect product visibility.

4.4 Brand and category validation

Before creating or updating a series:

  • brandId is validated against the brands table. If not found, returns BRAND_NOT_FOUND.
  • categoryId is validated against the categories table. If not found, returns CATEGORY_NOT_FOUND.
  • Both validations run in parallel via Promise.all().

4.5 Reorder behavior

  • Reorder accepts an array of { id, order } objects.
  • Each item is updated individually via separate UPDATE queries executed in parallel via Promise.all().
  • There is no wrapping transaction; each update is atomic.
  • List cache is invalidated after all updates complete.
  • No validation is performed that all provided ids exist.

4.6 Cache invalidation on write

  • create invalidates the list cache pattern catalog:brand-series:list:*.
  • update invalidates list cache + the old slug detail cache. If slug changed, also invalidates the new slug detail cache.
  • delete invalidates list cache + the deleted series slug detail cache.
  • reorder invalidates list cache only (detail cache is unaffected by order changes).

4.7 No async processing

Unlike Order, this module has no:

  • queue workers or background jobs
  • outbox event dispatching
  • async request tracking
  • scheduled maintenance tasks

5. Caching Strategy

Read-side keyspaces:

  • catalog:brand-series:list:* -- customer list responses
  • catalog:brand-series:detail:slug:* -- customer detail responses

Cache behavior:

  • Customer service uses getOrSet pattern: check cache first, populate on miss.
  • Admin service does NOT cache reads; all admin queries hit the database directly.
  • Cache entries use the TTL specified by CATALOG_CACHE_TTL_SECONDS.
  • Fallback chain: CATALOG_CACHE_TTL_SECONDS -> REDIS_CACHE_TTL_SECONDS -> 300 (default).

Customer cache key construction:

catalog:brand-series:list:brand:<brandId>-cat:<categoryId>-page:<page>-size:<size>
catalog:brand-series:detail:slug:<slug>

Invalidation:

  • Admin write operations use RedisCacheService.invalidatePattern() for list cache.
  • Detail cache invalidation uses RedisCacheService.invalidate() with the exact key.

6. Rate Limiting Strategy

This module does not apply its own rate limiting.

Customer endpoints are public and currently unthrottled. If storefront scraping or API abuse becomes an issue, rate limiting can be added at:

  • application level using @Throttle() decorator from @nestjs/throttler
  • infrastructure level via reverse proxy (Cloudflare rate limiting, Nginx limit_req)

Admin endpoints are protected by JWT authentication + RoleGuard + permission checks. Global admin rate limiting defaults apply but no module-specific throttle configuration is set.

7. Error and Resilience Contracts

Representative API/domain errors:

HTTPerrorCodeMessage
404BRAND_SERIES_NOT_FOUNDBrand series not found.
404BRAND_NOT_FOUNDBrand not found.
404CATEGORY_NOT_FOUNDCategory not found.
409BRAND_SERIES_DUPLICATEA series for this brand and category already exists.
409BRAND_SERIES_SLUG_EXISTSBrand series slug already exists.
403-Forbidden resource.
429RATE_LIMIT_EXCEEDEDToo many requests. Please try again later.
503ERR_CONFIG_INVALIDConfiguration value is missing/invalid.

Resilience notes:

  • No retry or backoff logic (no async processing).
  • DB unique constraint violations are caught and mapped to ConflictException.
  • DB FK violations cascade on brand/category delete.
  • All operations are idempotent except create (which may return duplicate errors).

8. Performance Notes

  • Admin and customer list queries use select projections (never SELECT *). Only the columns present in the response DTO are selected.
  • List queries use PaginationUtil.getDrizzleParams() for normalized pagination.
  • Joins on brands and categories are INNER JOIN (both FKs are required/not-null, so every series has a valid brand and category).
  • Customer list query includes an implicit is_active = true filter, and the brand_series_active_order_idx composite index covers this filter + sort.
  • Customer detail endpoint includes a correlated subquery for productCount:
    SELECT COUNT(*)::int FROM product p
    WHERE p.series_id = brand_series.id
      AND p.status = 'published'
      AND p.is_sellable = true
    This is a per-row query that runs once per detail request. For brands with many series, list queries avoid this subquery.
  • Cache getOrSet pattern prevents repeated DB hits for popular series on the storefront.

9. Backend Diagram

10. Release/QA Checklist

  • Admin CRUD operations enforce correct BrandSeries_* permissions.
  • Create validates brand and category existence before insert.
  • Create with duplicate (brandId, categoryId) returns BRAND_SERIES_DUPLICATE (409).
  • Slug generation is conflict-safe: creating "Dell Laptops" when slug "dell-laptops" exists produces "dell-laptops-1".
  • Update with name change regenerates slug and invalidates old + new detail cache.
  • Customer list returns only isActive = true series.
  • Customer detail returns productCount matching published + sellable products only.
  • Customer detail for inactive series returns 404.
  • Mobile-composed routes (/api/mobile/brand-series/...) resolve correctly.
  • Reorder updates all items and invalidates list cache.
  • Delete cascades are safe: deleting a brand or category cascades to its series.
  • Cache TTL is honored; admin writes invalidate customer cache correctly.
  • Admin list supports brandId, categoryId, isActive filters independently and in combination.

11. File Map

ConcernFile PathPurpose
Admin controllerapps/api/src/modules/catalog/admin/brand/brand-series/brand-series-admin.controller.tsHTTP surface for admin CRUD operations
Admin serviceapps/api/src/modules/catalog/admin/brand/brand-series/brand-series-admin.service.tsBusiness logic: CRUD, slug generation, cache invalidation
Admin moduleapps/api/src/modules/catalog/admin/brand/brand-series/brand-series-admin.module.tsModule composition and DI registration
Customer controllerapps/api/src/modules/catalog/customer/brand/brand-series/brand-series-customer.controller.tsHTTP surface for public listing and detail
Customer serviceapps/api/src/modules/catalog/customer/brand/brand-series/brand-series-customer.service.tsBusiness logic with caching layer and product count query
Customer moduleapps/api/src/modules/catalog/customer/brand/brand-series/brand-series-customer.module.tsModule composition and DI registration
DTOs (admin)apps/api/src/modules/catalog/admin/brand/brand-series/dto/CreateBrandSeriesDto, UpdateBrandSeriesDto, FetchBrandSeriesAdminDto, ReorderBrandSeriesDto, BrandSeriesAdminResponseDto
DTOs (customer)apps/api/src/modules/catalog/customer/brand/brand-series/dto/FetchBrandSeriesDto, BrandSeriesListItemDto, BrandSeriesDetailDto
Drizzle schemapackages/db/src/schema/catalog/brand-series.tsTable definition, indexes, relations

12. Environment Variables

VariableDefaultDescription
CATALOG_CACHE_TTL_SECONDS300Cache TTL for customer-facing catalog list/detail endpoints (also used by brand, category modules)
REDIS_CACHE_TTL_SECONDS300Fallback cache TTL if CATALOG_CACHE_TTL_SECONDS is not set

No module-specific rate limiting env vars exist for this module. Admin endpoints rely on global NestJS throttler defaults.

Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.


See Also

  • Feature Guide: See Brand Series - Feature List Section 5 (Key Business Rules) for domain invariants.
  • API Reference: See Brand Series - API & Integration Guide Section 11 (Endpoint Reference + Payload Cheatsheet) for API contracts.
  • DB Schema: See packages/db/src/schema/catalog/brand-series.ts for the full Drizzle schema definition.
  • Brand Module: See /docs/developer/catalog/brand for brand-level documentation.
  • Category Module: See /docs/developer/catalog/category for category-level documentation.