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
PUBLISHEDproducts whose Work permitsDISTRIBUTEto 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$variableplaceholders, 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% Offin 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:
.99endings in the UK and US,.95in 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)
Related pages
- Products overview — the Work → Expression → Manifestation → Product model catalogues draw on.
- Product types — the product attributes catalogue rules reference.
- Product search — embeddings and the client-side taxonomy catalogues complement.
- Distribution — the service that consumes membership events and ships to channels.
- Contracts & Rights — where the distribution permissions rules can reference are resolved.
- Explorer — Chapter IX — catalogues, visualised.