Catalog Service

catalog-service is a Quarkus service that owns the store’s product graph: products, variants, option types, categories, pricing, media, and the search index. It is the authoritative source of truth for all catalog data across every store. Storage is PostgreSQL with store_id as a discriminator column for multi-tenant isolation. Media assets are stored in AWS S3. Product search is served by a dedicated OpenSearch cluster — PostgreSQL full-text search is not used for products.

Product Model#

The product model follows a Shopify-style option type → variant pattern.

Products#

Each product record holds metadata only: product_id UUID PK, store_id, status (DRAFT / ACTIVE / ARCHIVED), created_at, updated_at.

All display fields are stored in a separate product_translations table keyed by (product_id, store_id, locale). Each row holds name, description, slug, and meta fields (meta_title, meta_description). The slug column has a UNIQUE constraint on (store_id, locale, slug) — slugs must be unique per store per locale.

Option Types and Values#

option_types defines the structured attribute dimensions for a product (Color, Size, Material). Columns: option_type_id, product_id, store_id, name, position.

option_values holds the valid values per option type (Red, Blue, S, M, L). Columns: option_value_id, option_type_id, product_id, store_id, value, position.

Variants#

variants are the concrete, purchasable combinations of option values. Each variant carries its own sku, barcode, weight_grams, and status (ACTIVE / INACTIVE).

variant_option_values is the junction table that maps which option values compose each variant: (variant_id, option_value_id, store_id).

Pricing#

Price Lists#

price_lists are named pricing configurations per store — for example, Retail, Wholesale, VIP. Each price list is linked to a customer_group that determines which shoppers see it. Columns: price_list_id, store_id, name, customer_group.

Variant Prices#

variant_prices stores the actual prices per variant per price list per currency. Columns:

ColumnTypeNotes
variant_idUUID FK
price_list_idUUID FK
currencyCHAR(3)ISO 4217
amountNUMERIC(12,4)Sell price
compare_atNUMERIC(12,4)Original “was $X” price; nullable
effective_fromTIMESTAMPTZNullable — no start bound
effective_toTIMESTAMPTZNullable — no end bound

UNIQUE constraint on (variant_id, price_list_id, currency).

Price Resolution#

Callers pass price_list_id and currency; catalog-service returns the matching variant_prices row. It is the caller’s responsibility to resolve the customer’s price_list_id — cart-service and checkout-service obtain this from the customer’s JWT claims or a prior call to customer-service. A currency converter layer can be added later without schema changes.

Categories#

categories is a self-referencing table that models a tree hierarchy. parent_id is a nullable FK back to category_id — a null parent indicates a root node.

category_translations is keyed by (category_id, store_id, locale) and holds name, slug, and description.

product_categories is the junction table that assigns products to category nodes. A product can belong to multiple nodes simultaneously. The position INT column controls display ordering within each category.

Media#

product_media stores asset references returned from S3. Columns: media_id, product_id, optional variant_id FK (null = applies to the whole product), store_id, s3_url, alt_text JSONB (locale-keyed), position, type (IMAGE / VIDEO).

Upload flow: the admin SPA posts the file to catalog-service via REST → catalog-service streams the upload to S3 → the returned S3 URL and metadata are written to product_media.

Stock Status#

Catalog does not own inventory data. For every product or variant fetch, catalog-service makes a gRPC call to inventory-service (GetStockLevels) and merges the returned stock status into the response payload. SmallRye Fault Tolerance circuit breaking is applied to this call to protect against inventory-service unavailability.

Search — OpenSearch#

A separate OpenSearch (Elasticsearch-compatible) cluster handles all product search. PostgreSQL full-text search is not used for this purpose.

catalog-service runs an internal Kafka consumer subscribed to catalog.product.* topics. On each event, it upserts a product document into the OpenSearch index.

Index Document Structure#

Each document contains: product_id, store_id, status, locale-keyed JSONB fields (name, description, slug), categories[], and variants[] (each with sku and option values). Prices are not stored in the index — they are always fetched live from PostgreSQL at hydration time.

Search Query Flow#

The GraphQL products(search: "...") resolver calls OpenSearch to retrieve matching product_ids, then hydrates the full product records from PostgreSQL. This keeps the index lean and prices accurate.

gRPC Interface#

These methods are exposed to internal callers only. All require store_id.

MethodCalled ByPurpose
GetVariant(variant_id, store_id)cart-serviceValidate variant at add-to-cart; returns a VariantSnapshot for display
GetVariantPrices(variant_ids[], price_list_id, currency, store_id)cart-serviceCart display prices
GetVariantPricesForCheckout(variant_ids[], price_list_id, currency, store_id)checkout-serviceAuthoritative price lock at checkout; used to compute order totals

GraphQL Subgraph#

catalog-service participates in Apollo Router federation as a subgraph.

Types#

Product@key(fields: "id"). Fields: name, description, slug, options, variants, categories, media.

Variant — Fields: id, sku, optionValues, price(priceListId, currency), compareAtPrice, stockStatus, media.

Category — Fields: id, name, slug, parent, children, products(page, pageSize, sort).

Reviews and average ratings are not on this service — they are owned by review-service and federated through Apollo Router.

Queries#

QuerySignature
productproduct(id: ID, slug: String): Product
productsproducts(categoryId, search, sort, page, pageSize): ProductConnection
categorycategory(id: ID, slug: String): Category
categoriescategories(parentId: ID): [Category]

REST Interface (Admin SPA)#

Full CRUD is exposed over REST for all catalog entities: products, variants, option types, option values, categories, price lists, variant prices, and media upload.

Kafka Events Published#

TopicPayloadConsumers
catalog.product.createdproductId, storeIdOpenSearch indexer (internal), BI
catalog.product.updatedproductId, storeId, changedFields[]OpenSearch indexer (internal), BI
catalog.product.archivedproductId, storeIdOpenSearch indexer (internal), BI
catalog.variant.price_updatedvariantId, storeId, priceListId, currency, newAmountmarketing-service (promotion engine), BI

Vault Configuration#

Key pathPurpose
ss3/kv/catalog-service/db.urlPostgreSQL JDBC URL
ss3/kv/catalog-service/db.usernameDatabase username
ss3/kv/catalog-service/db.passwordDatabase password
ss3/kv/catalog-service/s3.bucketMedia S3 bucket name
ss3/kv/catalog-service/s3.regionAWS region for S3
ss3/kv/catalog-service/opensearch.endpointOpenSearch cluster endpoint
ss3/kv/catalog-service/opensearch.usernameOpenSearch username
ss3/kv/catalog-service/opensearch.passwordOpenSearch password
ss3/kv/shared/Shared Kafka bootstrap and OTel config