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:
| Column | Type | Notes |
|---|---|---|
variant_id | UUID FK | |
price_list_id | UUID FK | |
currency | CHAR(3) | ISO 4217 |
amount | NUMERIC(12,4) | Sell price |
compare_at | NUMERIC(12,4) | Original “was $X” price; nullable |
effective_from | TIMESTAMPTZ | Nullable — no start bound |
effective_to | TIMESTAMPTZ | Nullable — 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.
| Method | Called By | Purpose |
|---|---|---|
GetVariant(variant_id, store_id) | cart-service | Validate variant at add-to-cart; returns a VariantSnapshot for display |
GetVariantPrices(variant_ids[], price_list_id, currency, store_id) | cart-service | Cart display prices |
GetVariantPricesForCheckout(variant_ids[], price_list_id, currency, store_id) | checkout-service | Authoritative 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#
| Query | Signature |
|---|---|
product | product(id: ID, slug: String): Product |
products | products(categoryId, search, sort, page, pageSize): ProductConnection |
category | category(id: ID, slug: String): Category |
categories | categories(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#
| Topic | Payload | Consumers |
|---|---|---|
catalog.product.created | productId, storeId | OpenSearch indexer (internal), BI |
catalog.product.updated | productId, storeId, changedFields[] | OpenSearch indexer (internal), BI |
catalog.product.archived | productId, storeId | OpenSearch indexer (internal), BI |
catalog.variant.price_updated | variantId, storeId, priceListId, currency, newAmount | marketing-service (promotion engine), BI |
Vault Configuration#
| Key path | Purpose |
|---|---|
ss3/kv/catalog-service/db.url | PostgreSQL JDBC URL |
ss3/kv/catalog-service/db.username | Database username |
ss3/kv/catalog-service/db.password | Database password |
ss3/kv/catalog-service/s3.bucket | Media S3 bucket name |
ss3/kv/catalog-service/s3.region | AWS region for S3 |
ss3/kv/catalog-service/opensearch.endpoint | OpenSearch cluster endpoint |
ss3/kv/catalog-service/opensearch.username | OpenSearch username |
ss3/kv/catalog-service/opensearch.password | OpenSearch password |
ss3/kv/shared/ | Shared Kafka bootstrap and OTel config |