Development
8 min readA merchandiser at a telecom provider needs every new handset to carry a Network value, such as 5G or 5G Advanced. It has to show on the product page, be filterable in search, and be something a promotion can target. In most commerce platforms that request becomes a ticket: someone adds a column, updates an API contract, redeploys, and reindexes.
Now suppose the same need comes from a school-products manufacturer. It runs one central catalog that feeds roughly 10,000 affiliate shop catalogs, and each shop needs its own subset of colors, its own defaults, and the option to override a single value without losing every future update from headquarters.
Those two scenarios shaped Broadleaf's Characteristics, part of the merchandising suite, over the last two major releases. We introduced characteristics as part of our telco merchandising work. This post covers what happened next: what we added, what broke under real-world scale, and how the model works today.
A Characteristic is a typed property defined on a Business Type (also called a Product Type), such as Phone, Apparel, or Service Plan. Each characteristic has:
The admin builds each product's maintenance form from its Business Type's characteristics. Add one, and the new field appears with no code change and no deployment.
That was a deliberate choice, made while building toward TM Forum's TMF620 catalog standard: extend the product model via a schema that changes with the business, or a stable schema that carries the extensions as data. Teams on the schema path told the same story: every product-model change meant reprogramming the middleware around it. Characteristics keep the API contract fixed while the product model keeps changing. That choice paid off: Business Types already gave us a data-driven product model, so characteristics extended it rather than replacing it, and TMF620's product specifications map naturally onto both.
Real catalogs are hierarchical. A Flagship Phone is still a Phone: same battery, memory, camera, and display characteristics, plus a few of its own.
Business Types can now inherit from other Business Types. A child type receives every characteristic from its ancestors, and the admin grid labels each one so it's clear where it came from:
When the same characteristic is defined at more than one level of the hierarchy, the closest ancestor wins. Form customizations made on a parent type, such as groupings and field placement, also carry down to its children. You define Phone once and adjust it only where the flagship is actually different.
A characteristic that only displays on a product page is useful. One that drives pricing and shopper choice is much more valuable. Characteristics can now do all of this:
The school-products manufacturer's deployment was the stress test: one central catalog feeding about 10,000 affiliate catalogs through catalog inheritance, each affiliate customizing what it sells. Supporting that produced three new capabilities:
The biggest lesson was structural. Product characteristics used to be stored as part of the product's own record and tracked together as a single unit. At 10,000-catalog scale that broke: an affiliate changing one characteristic overrode all of them for that product, and updates from the central catalog stopped reaching any of them.
In 2.3.0, each Product Characteristic became an independently tracked record, so each one can be overridden on its own. An affiliate can change one value and keep inheriting everything else. The upgrade's migration also cleans up values that only repeated what a parent catalog already had, so those products go back to inheriting.
This produced a principle we now apply throughout: persist only real overrides. When a product is created, values matching its Business Type's defaults aren't stored on it, so upstream changes keep flowing to it. Imports store only what you actually supply, and an override always takes precedence over an inherited value.
Characteristics that generate variants also generate volume. Products with hundreds of variants meant loading a page or adding to cart pulled far more variant data than the request needed.
In 2.3.0, callers can choose how much variant data to load: all of it, a lite version through the Lite Variants API, or none. Lite variants give the storefront a compact map from option selections to SKUs, resolving a shopper's choice without loading every variant in full. A call for a phone with Color and Capacity options looks like this:
GET /lite-variants/{productId}
[
{
"id": "VAR-4471-BLK-256",
"productId": "PROD-4471",
"sku": "SKU-4471-BLK-256",
"optionValues": { "color": "Black", "capacity": "256GB" }
},
{
"id": "VAR-4471-BLU-512",
"productId": "PROD-4471",
"sku": "SKU-4471-BLU-512",
"optionValues": { "color": "Blue", "capacity": "512GB" }
}
]
The storefront never has to pull full variant records just to answer "does this combination exist, and what's its SKU." In our performance testing on a product with 561 variants, add-to-cart time dropped from about 4 seconds to about 1.6 seconds.
Characteristics are flexible, and that flexibility has trade-offs to know about up front:
If your product model changes often, because you sell telecom plans, configurable goods, or anything where "just add a field" comes up weekly, characteristics let your merchandising team make those changes without an engineering release. Inheritance keeps large type hierarchies manageable, and rule-builder and pricing targeting mean a new field starts working the day it's created. The lessons from a 10,000-catalog deployment are now part of the core framework, not left for each implementation to work out.
Want to see the data model yourself? The Characteristic and Business Type reference docs are on Broadleaf Dev Central.
For the original introduction of characteristics and the Business Type model they extend, see Beyond the Default: Customizing Product Data Models with Characteristics.