Skip to content

Catalogues

A catalogue is a curated subset of products chosen for one specific audience or destination. Packt sells the same Products through many fronts — packtpub.com, gamedevassembly.com, the Amazon Kindle store, a Shopify storefront, a subscription app — and almost none of them should show the whole catalogue. A catalogue is the bridge between the full product set and the particular slice of it that appears in a given place.

Most catalogues are too large to hand-pick and too dynamic to freeze, so membership is defined two ways at once: an editor can manually pick individual products, and a set of rules can pull in every product that matches some criteria. Rules are evaluated continuously, so as products are published, retired, re-priced, or re-tagged they flow into and out of the catalogue on their own — no one has to remember to add the new release to every storefront.

A catalogue only decides which products belong. It does not deliver them, and it does not decide how they are arranged on the page. When a product enters or leaves a catalogue, the Product Management service announces it, and the Distribution service reacts — pushing the change to Amazon, Shopify, or a storefront feed. Keeping "which products" (the catalogue) separate from "how they ship" (Distribution) is what lets one catalogue drive several very different destinations.

This page covers how membership is built, the states a catalogue moves through, how a catalogue entry can present a product differently from its base record, and how membership hands off to Distribution.

Manual and rule-based membership

Products join a catalogue in two complementary ways, and a single catalogue can use both at once.

  • Manual picks — an editor explicitly adds individual products. This suits editorial curation, a featured collection, or a small catalogue where every inclusion is a deliberate choice. Manually picked products are always included, whether or not they also match a rule.
  • Rules — filters that automatically include every product matching their criteria. Rules are evaluated continuously, so the catalogue expands and contracts on its own as products change.

Rules can reference any product attribute — type, language, price, lifecycle state, tags, authors — or any Content Lake attribute reachable through a product's content, such as Resource tags, Knowledge Graph entities, or the rights resolved on the governing Work. Some worked examples:

  • All English-language Book products priced under £30 whose content carries the tag kubernetes.
  • All PUBLISHED products whose Work permits DISTRIBUTE to the Amazon channel.
  • All Video Course products by a particular author.

The second example shows the catalogue reading the rights model rather than duplicating it. Rights are held once on the Work and resolved into a projection the rule can test, so a catalogue can include exactly the products that a contract actually permits to be distributed to a given channel — and drop them the moment that permission lapses. See Contracts & Rights.

flowchart LR
    R["Rule-matched products<br/>(evaluated continuously)"] --> CAT["Catalogue"]
    M["Manual picks<br/>(always included)"] --> CAT
    CAT --> D["Membership events<br/>→ Distribution"]

Rules read attributes; they do not enforce rights

A rule that references distribution permission is a filter, not an enforcement gate. It decides membership from the rights already resolved on the Work. The actual rights check at the point of shipping is Distribution's job — the catalogue only narrows the field. See Catalogues and Distribution.

Catalogue structure

A catalogue is a small record: an identity, an owner, its two kinds of membership, and its state.

Field Type Description
id string Catalogue ID (cat_ + UUIDv7)
name string Human-readable name
description string Purpose and audience
owner_id string The owning User (u_)
rules rule[] Inclusion rules, evaluated continuously
manual_products string[] Explicitly included product IDs (prod_)
state string draft, active, frozen, or archived

Every catalogue has exactly one owner — the person accountable for its purpose, its rules, and its overrides. Ownership makes it obvious who to contact when membership looks wrong, when rules need updating, or when a frozen catalogue needs unfreezing. Ownership can be transferred but never removed: a catalogue always has an owner.

Catalogue lifecycle

A catalogue moves through four states. The important ones are ACTIVE and FROZEN, which can be flipped back and forth, and ARCHIVED, which is the end of the line.

stateDiagram-v2
    [*] --> DRAFT
    DRAFT --> ACTIVE
    ACTIVE --> FROZEN
    FROZEN --> ACTIVE
    ACTIVE --> ARCHIVED
    FROZEN --> ARCHIVED
    ARCHIVED --> [*]
State What it means
DRAFT Exists but is not live. Rules can be configured and tested and overrides previewed, but no membership events are emitted and downstream systems do not read it
ACTIVE Live. Rules are evaluated continuously, manual picks and overrides can change, and membership events are emitted
FROZEN Live and read downstream, but pinned. No rule re-evaluation, no new manual picks, no override changes — the membership set is fixed
ARCHIVED End of life. Not read downstream; retained for historical reference

ACTIVE and FROZEN are reversible. Freezing pins a catalogue's membership so it cannot drift — useful for a seasonal campaign where the line-up must not change under a promotion — and unfreezing returns it to continuous evaluation when the campaign ends. ARCHIVED is terminal.

DRAFT exists for staging. A team can configure rules, check which products they match, and preview overrides before anything goes live. Promoting a catalogue from DRAFT to ACTIVE is the moment it starts emitting membership events, which is what first triggers Distribution to sync.

Catalogues and taxonomy

Catalogues answer a different question from taxonomy, and it is worth keeping the two apart.

  • A catalogue defines which products are available in a context — the set.
  • A taxonomy defines how those products are organised within that context — the structure of categories and subcategories.

A storefront has one catalogue and one taxonomy. The catalogue might hold 5,000 products; the taxonomy arranges those 5,000 into a browsable tree. Both are scoped to the client application, and they vary independently: packtpub.com and gamedevassembly.com have different catalogues (different product sets) and different taxonomies (different category schemes) laid over those sets.

Taxonomy is deliberately a client concern — the Product Management service does not store or enforce one. It exposes product embeddings so each storefront can build its own categorisation, and never has to know about any storefront's scheme. See Product search.

Catalogues and Distribution

The catalogue decides scope; Distribution decides delivery. They are separate services with separate lifecycles, connected by events rather than direct calls.

flowchart TD
    CAT["Catalogue<br/>(Product Management)"] -->|"defines the product set"| DIST["Distribution service"]
    DIST -->|"delivers listings, applies rights"| CH["Channel<br/>(Amazon, Shopify, …)"]

When Distribution needs to know which products belong on a channel, it reads the catalogue. When a product enters or leaves a catalogue — because a manual pick was added or removed, or because a rule match changed after a product was published, re-priced, or re-tagged — the Product Management service emits a membership event, and Distribution reacts by syncing inventory to Shopify, updating listings on Amazon, or refreshing a storefront's feed.

This is why third-party inventory systems such as Shopify and WooCommerce are Distribution targets, not catalogue concerns. The catalogue names the product set; Distribution handles the sync and applies the rights policy at the channel boundary.

In design

Distribution and the channel sync it performs are still in design. Catalogue membership and its events are shipped behaviour; treat the downstream Distribution mechanics described here as the intended shape rather than settled detail. See Distribution.

Membership events (for the technically curious)

Catalogue events are published to the platform-products event bus, following the platform's platform.products.{resource}.v1.{action} naming. The two that drive Distribution sync are the membership pair — they fire whenever a product enters or leaves a catalogue, for any reason (manual pick, rule-match change, or an attribute update that changes rule evaluation).

Event Fires when
platform.products.catalogue.v1.created A catalogue is created
platform.products.catalogue.v1.updated Rules or metadata change
platform.products.catalogue.v1.archived A catalogue is archived
platform.products.catalogue.v1.product_added A product enters a catalogue
platform.products.catalogue.v1.product_removed A product leaves a catalogue

Catalogue overrides

The same product often needs to look different in different places — a promotional title on one storefront, a translated title on another, the plain product record on a third. Catalogue overrides let a catalogue entry diverge from the base product without ever touching the base record. Each entry can override any product attribute: title, subtitle, description, cover images, pricing, or other metadata.

Every overridable field carries a sync mode that decides how the entry's value relates to the base product:

  • sync (the default) — the entry tracks the base product. When the base title changes, the entry's title changes with it.
  • override — the entry uses a fixed replacement value that does not change when the base product changes.
  • template — the entry uses a template that references base product fields through $variable placeholders, resolved fresh each time the entry is read.
flowchart LR
    BP["Base product<br/>title: Go Microservices"] --> S["sync<br/>→ Go Microservices"]
    BP --> O["override<br/>→ Microservices mit Go"]
    BP --> T["template<br/>$title — Now 70% Off<br/>→ Go Microservices — Now 70% Off"]

Field templates

Field templates use $variable syntax to pull in base product fields. The value is resolved at read time against the current base product, so a promotional wrapper stays correct even after the base title is edited.

Template Base title Resolved value
$title — Now 70% Off Go Microservices Go Microservices — Now 70% Off
$title (${edition}e) Go Microservices Go Microservices (2e)
Learn $title Kubernetes Learn Kubernetes

The available variables:

Variable Source
$title Base product title
$subtitle Base product subtitle
$description Base product description
$authors Comma-separated author names
$edition Edition number
$language BCP-47 language code
$price Formatted base price

Because templates are evaluated at read time and never stored resolved, changing the base title from "Go Microservices" to "Go Microservices in Production" instantly updates every entry that uses $title — Now 70% Off, across every catalogue, with no re-editing.

Override scope

Overrides apply per catalogue entry, never globally. One product can be presented four different ways at once:

  • base title Go Microservices in the product record;
  • template title $title — Now 70% Off in the Shopify catalogue;
  • override title Microservices mit Go in the German storefront catalogue;
  • plain sync (the base title) in the Amazon catalogue.

Each catalogue manages its own overrides independently, and none of them alters the base product. This is what lets the same underlying product carry a promotional voice in one place and a localised one in another without forking the record.

Catalogue entry structure

A catalogue entry is a thin layer over the base product: a reference, how the product got in, and its per-field overrides.

Field Type Description
product_id string Reference to the base product (prod_)
source string rule, manual, or both
overrides map Field name → override config
overrides.{field}.mode string sync, override, or template
overrides.{field}.value string Static value or template string

When no override is configured for a field, it defaults to sync and the base product value is used as-is.

Destination templates

Overrides decide how a product reads in a catalogue; a destination template decides what a catalogue must contain to be accepted by the system it feeds. Third-party destinations each have their own required fields — Amazon listings want browse nodes, bullet points and search terms; a Google Shopping Merchant Centre feed requires a GTIN, a brand, a Google product category and availability; Shopify expects a vendor, a product type and variants. None of those fields live on a Packt product record, and every destination's list is different.

A destination template captures that requirement list once — the field names, types, whether each is required, and the destination's constraints (Amazon allows five bullet points; a Merchant Centre title has a length cap). A catalogue that feeds such a destination builds against the template: it declares which template it targets, and carries a field mapping that says where each template field comes from — a product attribute (the ISBN becomes the GTIN), an override, a field template string, or a fixed value ("Packt" as the brand).

flowchart LR
    P["Product record<br/>+ overrides"] -->|field mapping| T["Destination template<br/>(Google Merchant feed)"]
    T -->|"validated entries"| D["Distribution<br/>→ the feed"]
    T -.->|"feed-blocked entries<br/>+ validation report"| R["Catalogue owner"]

Templates ship for the common destinations and are versioned — when Amazon changes its requirements, a new template version appears and catalogues migrate deliberately rather than breaking overnight.

The pay-off is that feeds fail loudly at the catalogue, not silently at the destination. Every entry is continuously validated against the template: an entry missing a required field, or breaking a constraint, stays in the catalogue but is marked feed-blocked — Distribution skips it, and the catalogue's validation report tells the owner exactly which product failed which field and why. You find out about the missing ISBN from your own report, not from a rejected feed three days later.

Prices in a catalogue

A product's prices live on the product — a base price plus explicit per-territory prices, each an amount in a named currency, with no automatic conversion. What a buyer pays is decided by the buyer's territory: their territory's price if one is set, otherwise the base price. A price set in Indian rupees for India is exactly that — buyers in India pay it, and a buyer anywhere else never sees it; they fall through to their own territory's price or the base price. Regional pricing needs no special mode.

Catalogues add two things on top:

  • Price overrides. A catalogue entry can price a product differently from the base record — an explicit price (used exactly as typed) or a computed one, such as a percentage promotion.
  • A price policy for computed prices. Markets disagree about what a price should look like: .99 endings in the UK and US, .95 in parts of Europe, whole rupees in India. Each catalogue carries a policy — a rounding direction plus per-currency endings — that every computed price is snapped to. A 30% promotion on £24.99 computes £17.49 and lands at £17.99; the same promotion on ₹1,999 computes ₹1,399.30 and lands at ₹1,400. Prices a person set explicitly are never rounded — typed prices mean what they say.
A catalogue entry with mixed sync modes (JSON)
{
  "product_id": "prod_go_microservices",
  "source": "both",
  "overrides": {
    "title": {
      "mode": "template",
      "value": "$title — Now 70% Off"
    },
    "description": {
      "mode": "sync"
    },
    "cover_image": {
      "mode": "override",
      "value": "ast_go_micro_promo_cover"
    }
  }
}