Data Modeling with the SAP Commerce Type System: Decisions That Age Well
How to design items.xml changes you will not regret: extend versus subtype, deployment tables and typecodes, relations versus collections, dynamic attributes, and cleaning up the type system debt you already have.
Type system changes are the least reversible decisions on an SAP Commerce project. Code can be refactored in a sprint; a wrong deployment table or a subtype hierarchy that spread through five years of data takes a migration project to unwind. This guide covers the decisions that matter in *-items.xml, in the order you will face them, with the failure modes each wrong choice produces.
One process rule before the design rules: every items.xml change is followed by a type system update (ant updatesystem or the equivalent deployment flag), and the update is tested against a copy of real data before it reaches production. Schema drift between what your items.xml declares and what the database holds is a background radiation of weird bugs.
Extend the Type or Subtype It?#
The most frequent modeling decision, and the one teams most often get wrong by defaulting to subtyping. The decision table:
| Situation | Choice | Why |
|---|---|---|
| The new attribute applies to all instances of the type | Extend the existing type | Every instance can carry the value; no casts needed anywhere |
| The attribute only makes sense for a distinct subset with its own lifecycle | Subtype | The subset is a real concept, not just a flag |
| You need to re-declare an existing attribute (change optionality, default, type) | Subtype | Re-declaration requires it |
Extending the existing type looks like this and is almost always the right call for cross-cutting attributes:
<itemtype code="Product" autocreate="false" generate="false">
<attributes>
<attribute qualifier="sustainabilityRating" type="java.lang.String">
<description>Acme sustainability grade, populated by PIM feed</description>
<modifiers read="true" write="true" search="true" optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
The autocreate="false" generate="false" pair is what says "modify, do not redefine".
Why unnecessary subtyping hurts, concretely:
- Business users can still instantiate the supertype. If your storefront logic requires
AcmeCategory.enabledForFrontendand a Backoffice user creates a plainCategory, the storefront meets an instance that lacks the attribute your code assumes. The type system will not protect you; it did exactly what you told it. - Platform APIs return the supertype.
CategoryServicehands youCategoryModel, and your code fills up withif (category instanceof AcmeCategoryModel)casts. Each one is a branch someone will forget. - Queries get harder. FlexibleSearch against
{AcmeCategory}misses plain categories; against{Category}it returns instances your code then filters by type. Either way you are paying complexity tax on every query forever.
When some instances genuinely form a distinct concept (a SubscriptionProduct with billing attributes that mean nothing on physical products), subtype without guilt, and consider re-declaring key attributes so platform flows return your subtype where possible.
A third option the platform makes easy to forget: composition. A new type holding the extra data with a relation to the core type keeps the core untouched and the extension optional per instance:
<itemtype code="ProductComplianceInfo" extends="GenericItem" autocreate="true" generate="true">
<deployment table="prodcompliance" typecode="20115"/>
<attributes>
<attribute qualifier="certificateCode" type="java.lang.String">
<persistence type="property"/>
</attribute>
<attribute qualifier="expiryDate" type="java.util.Date">
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
<relation code="Product2ComplianceInfo" localized="false">
<sourceElement type="Product" qualifier="product" cardinality="one"/>
<targetElement type="ProductComplianceInfo" qualifier="complianceInfos" cardinality="many"/>
</relation>
Composition wins when the extra data has its own lifecycle (compliance certificates expire independently of the product), or when several types need the same bolt-on.
Deployment Tables and Typecodes#
Every concrete type that can be instantiated needs to map to a database table, and the rules here produce the platform's most infamous performance problems when ignored.
- Custom typecodes live in the range 10000 to 32767. Below 10000 is reserved for SAP; collisions produce initialization failures at best.
- Keep a typecode registry in the repository (a comment block at the top of your main items.xml works) so parallel feature branches do not both claim 20115.
- Every new top-level type gets its own deployment table. If you omit
<deployment>, instances land in the ancestor's table, and for types extendingGenericItemthat means the sharedgenericitemstable, where every query becomes a scan across unrelated types. - Subtypes normally share the parent's table. That is correct and efficient for true subtypes; the discriminator column handles it. Give a subtype its own deployment only when it will hold a large volume of data with a very different access pattern, and understand that platform joins across the hierarchy get more expensive when you do.
- Many-to-many relations need their own deployment table too, or they land in the shared
linkstable with the same scan problem:
<relation code="Product2Certification" localized="false">
<deployment table="prod2cert" typecode="20116"/>
<sourceElement type="Product" qualifier="products" cardinality="many"/>
<targetElement type="Certification" qualifier="certifications" cardinality="many"/>
</relation>
Relations, Collections, and Ordering#
The type system offers CollectionType attributes and first-class relations. The rule is short: prefer relations. A collection attribute serializes item PKs into a single column; it cannot be queried from the other side, cannot be joined efficiently, silently truncates when the serialized form outgrows the column, and hides referential integrity from the database entirely. Collections are acceptable only for small, stable value lists where none of that matters.
Two relation modifiers deserve respect:
ordered="true"adds a sequence number column and preserves ordering, at the cost of write amplification: inserting into the middle of a large ordered relation rewrites sequence numbers. Do not order relations that will hold thousands of elements unless the business truly needs stable manual ordering.- One-to-many relations can be navigated from the many side cheaply; the many-side getter on the one side (
product.getOrderEntries()style calls) loads the entire relation into memory. For large relations, that getter is a trap; query with FlexibleSearch and paginate instead.
Attribute Design Details That Compound#
optional="false"needs a default or a plan. A mandatory attribute added to a type with existing rows makes the type system update fail or leaves nulls that violate the declaration, depending on database. Add mandatory attributes in two steps: optional first plus a data migration, then flip to mandatory.unique="true"is a business key declaration, not an index. The platform enforces uniqueness through interceptors, not database constraints, and it does not automatically create an index. Declare the index explicitly for anything you query by:
<attribute qualifier="externalId" type="java.lang.String">
<modifiers optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
...
<indexes>
<index name="externalIdIdx" unique="true">
<key attribute="externalId"/>
</index>
</indexes>
- Localized attributes (
localized:java.lang.String) store per-language values in a separate localized properties table. They are the right tool for display text and the wrong tool for anything you filter or join on in bulk. - Dynamic attributes (
persistence type="dynamic") compute their value in anAttributeHandlerat read time. They are elegant for derived display values and lethal when the handler is expensive: Backoffice list views and OCC serialization will call the handler once per row per render. Keep handlers allocation-free and query-free. - Enums: use
enumtypewithdynamic="false"for closed sets the code switches on,dynamic="true"when business users may add values. Switching a static enum to dynamic later is easy; the reverse is a data cleanup exercise.
Catalog-Aware Types#
If a custom type belongs to a catalog (it should, whenever its lifecycle follows staged-to-online publishing), make it properly catalog-aware: a catalogVersion attribute marked unique together with the business key, and the catalogItemType configuration so synchronization and Backoffice treat it correctly:
<itemtype code="StoreLocatorEntry" extends="GenericItem" autocreate="true" generate="true">
<deployment table="storelocentry" typecode="20117"/>
<custom-properties>
<property name="catalogItemType"><value>java.lang.Boolean.TRUE</value></property>
<property name="catalogVersionAttributeQualifier"><value>catalogVersion</value></property>
<property name="uniqueKeyAttributeQualifier"><value>code</value></property>
</custom-properties>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<modifiers optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="catalogVersion" type="CatalogVersion">
<modifiers optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
Skipping this and pointing storefront queries at a non-versioned type works until the business asks why edits go live instantly with no staging, no preview, and no rollback. Retrofitting catalog awareness onto a populated type is a genuine migration.
Type System Cleanup: The Debt You Already Have#
Removing a type or attribute from items.xml removes it from the compiled type system. It does not remove the column, the table, or the data. Years into a project, the database carries orphaned columns from renamed attributes, whole tables from abandoned features, and serialized collection columns from refactored relations. The costs are real: backup size, migration time during upgrades, and confusion for every DBA and data migration that has to guess what p_oldloyaltytier is.
The discipline: every items.xml removal ships with a tested cleanup step in the deployment runbook. The platform's orphaned-type tooling (HAC shows orphaned types and columns after an update) tells you what is dangling; the removal itself is a scripted, reviewed DDL change executed after the release that stopped using the structure, not during it, so rollback stays possible.
Schedule a type system audit before any major upgrade. Migrating orphaned data to a new platform version is paying to move furniture into a house it will never be used in.
Review Checklist for items.xml Changes#
- Existing type extended rather than subtyped, unless the subset is a real concept or re-declaration forced it
- New top-level types and many-to-many relations have their own deployment table with a registered typecode in 10000-32767
- No new
CollectionTypeattributes where a relation belongs - Ordered relations justified by an actual business ordering requirement
- Mandatory attributes on populated types introduced via the two-step path
- Indexes declared for every attribute used in lookups;
uniquenot mistaken for an index - Dynamic attribute handlers cheap enough for list-view rendering
- Catalog-aware configuration present for staged content types
- Cleanup steps written for anything removed
- Type system update tested against production-shaped data
The type system rewards teams that treat it like a public API: deliberate changes, versioned decisions, and an allergy to "we can fix the model later". Later is where the migration budget goes.