The type system is the beating heart of SAP Commerce. It defines every data structure in the platform — from products and orders to customers and CMS components. Unlike traditional Java applications where you write JPA entities and manage database migrations with Flyway, SAP Commerce uses a metadata-driven approach: you declare your data model in XML, and the platform generates Java classes, database schemas, and runtime type metadata automatically.
This article is the definitive deep dive into that system. Whether you're a developer extending the OOTB model for the first time or a senior architect debugging type system internals, this guide covers everything from the XML syntax to the generated bytecode to the database tables underneath.
The Type Hierarchy
Every piece of data in SAP Commerce is an Item. The type hierarchy is:
┌─────────────┐
│ Item │ (abstract root — PK, creation/modification time)
└──────┬──────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────────▼───────┐ ┌─────▼──────┐ ┌──────▼──────┐
│ GenericItem │ │ CronJob │ │ Type │
│ (no deployment │ │ │ │ (metadata) │
│ needed) │ │ │ │ │
└────────┬───────┘ └────────────┘ └─────────────┘
│
┌─────────┼──────────┬──────────────┐
│ │ │ │
┌───▼──┐ ┌───▼───┐ ┌────▼────┐ ┌─────▼──────┐
│Product│ │ Order │ │Customer │ │Your Custom │
│ │ │ │ │(User) │ │ Type │
└──────┘ └───────┘ └─────────┘ └────────────┘
Core Meta-Types
The type system itself is described by meta-types — types that describe other types:
| Meta-Type | Description | Examples |
|---|---|---|
| ComposedType | Describes an item type (like a Java class definition) |
Product, Order, Customer
|
| AtomicType | Wraps a Java primitive or simple type |
java.lang.String, java.lang.Integer, java.util.Date
|
| CollectionType | A typed collection of another type |
StringCollection (List of Strings) |
| MapType | A typed key-value map |
localized:java.lang.String (language → string map) |
| EnumType | An enumeration with fixed or dynamic values |
ArticleApprovalStatus, OrderStatus
|
| RelationType | Describes a relationship between two ComposedTypes | Product2CategoryRelation |
You can think of ComposedType as analogous to java.lang.Class — it's the runtime descriptor for an item type. Every ComposedType has:
- A code (e.g.,
"Product") - A supertype (parent
ComposedType) - A set of AttributeDescriptor instances (the type's attributes)
- A deployment (database table mapping)
- A jaloclass (legacy Jalo layer class)
Attribute Persistence Modes
Every attribute in items.xml has a persistence mode that determines how its value is stored:
property — Database Column
<persistence type="property"/>
The standard mode. Creates a column in the type's deployment table. This is what you use 90% of the time. The column type is determined by the attribute's Java type:
| Java Type | Database Column (HANA) | Database Column (MySQL) |
|---|---|---|
java.lang.String |
NVARCHAR(255) | VARCHAR(255) |
java.lang.Integer |
INTEGER | INT |
java.lang.Long |
BIGINT | BIGINT |
java.lang.Double |
DOUBLE | DOUBLE |
java.lang.Boolean |
TINYINT | TINYINT |
java.util.Date |
TIMESTAMP | DATETIME |
java.math.BigDecimal |
DECIMAL(30,8) | DECIMAL(30,8) |
You can customize column size with:
<persistence type="property">
<columntype database="mysql">
<value>VARCHAR(1000)</value>
</columntype>
<columntype database="hana">
<value>NVARCHAR(1000)</value>
</columntype>
<columntype>
<value>java.math.BigDecimal</value>
</columntype>
</persistence>
dynamic — Computed at Runtime
<persistence type="dynamic" attributeHandler="myAttributeHandler"/>
Dynamic attributes have no database column. Instead, a Spring bean (DynamicAttributeHandler) computes the value on every read:
public class LoyaltyScoreHandler implements DynamicAttributeHandler<Double, CustomerModel> {
@Override
public Double get(CustomerModel model) {
Integer points = model.getLoyaltyPoints();
if (points == null) return 0.0;
// Complex calculation based on points, tier, and activity
double multiplier = getMultiplierForTier(model.getLoyaltyTier());
return points * multiplier;
}
@Override
public void set(CustomerModel model, Double value) {
// Read-only attribute — throw exception or ignore
throw new UnsupportedOperationException("loyaltyScore is read-only");
}
private double getMultiplierForTier(LoyaltyTier tier) {
if (tier == null) return 1.0;
switch (tier.getCode()) {
case "PLATINUM": return 2.0;
case "GOLD": return 1.5;
case "SILVER": return 1.25;
default: return 1.0;
}
}
}
Register in Spring:
<bean id="loyaltyScoreHandler"
class="com.mycompany.handlers.LoyaltyScoreHandler"/>
Use dynamic attributes when:
- The value is computed from other attributes
- The value comes from an external system (API call, cache lookup)
- You want a virtual attribute without database storage overhead
- The value changes frequently and caching would be stale
Avoid dynamic attributes when:
- You need to query/filter by this attribute in FlexibleSearch (dynamic attributes can't be used in WHERE clauses)
- The computation is expensive and the attribute is read frequently
- You need the value to persist across restarts
jalo — Legacy Mode
<persistence type="jalo"/>
Jalo persistence means the attribute getter/setter is implemented in the Jalo layer class. This is deprecated — never use this for new attributes. It exists only for backward compatibility with very old extensions.
cmp — Container Managed Persistence
An internal persistence mode used by the platform for certain system attributes. You should never use cmp for custom attributes.
Type Deployment: How Types Map to Database Tables
Understanding how the type system maps to relational storage is critical for debugging and performance.
Dedicated Table Deployment
When you specify <deployment table="my_table" typecode="25000"/>, the type gets its own table:
loyalty_tx table:
┌──────────────┬─────────────┬────────┬──────────┬─────────────────┬───────────┐
│ PK (BIGINT) │ TypePkString│ p_code │ p_points │ p_txdate │OwnerPkStr │
│ │ │ │ │ │ │
│ 8796093087744│ 8796... │ LT-001 │ 500 │ 2025-01-15 10:30│ null │
└──────────────┴─────────────┴────────┴──────────┴─────────────────┴───────────┘
Key columns automatically added by the platform:
| Column | Purpose |
|---|---|
PK |
Primary key (BIGINT) — generated by the PK generator |
TypePkString |
The PK of the ComposedType record (enables polymorphic queries) |
p_* |
Attribute columns (prefixed with p_ by convention) |
OwnerPkString |
Owner reference (used for partof relationships) |
createdTS |
Creation timestamp |
modifiedTS |
Last modification timestamp |
aCLTS |
Access control timestamp |
propTS |
Properties timestamp |
Single-Table Inheritance
If a type does not have a <deployment>, its attributes are stored in the parent type's table. For example, Customer extends User, and if Customer has no separate deployment, its custom attributes go into the users table:
users table:
┌────────┬──────────┬──────────┬────────────────┬───────────────┐
│ PK │ p_uid │ p_name │ p_loyaltytier │ p_loyaltypts │
│ │ │ │ (from Customer)│ (from Customer)│
└────────┴──────────┴──────────┴────────────────┴───────────────┘
This is efficient for small additions but can bloat the parent table if many subtypes add many attributes.
The props Table (Overflow)
When an attribute cannot fit in the type's deployment table (e.g., the type has no dedicated deployment, or the attribute is added to a type after initialization without running updatesystem), it goes to the props table:
props table:
┌────────────┬───────────┬────────────────┬──────────────┐
│ ITEMPK │ NAME │ LANGPK │ VALUE1 │
│ │ │ │ │
│ 8796... │ myattr │ null │ some_value │
└────────────┴───────────┴────────────────┴──────────────┘
The props table is essentially a key-value store. It's flexible but terrible for performance — every attribute access requires a separate lookup. This is why you should:
- Always define a
<deployment>for new types - Run
ant updatesystemafter adding attributes to existing types - Monitor
propstable usage with:
SELECT COUNT(*) FROM props;
-- If this is large (millions+), investigate which attributes are overflowing
The ydeployments Table
The platform maintains a ydeployments table that maps typecodes to table names:
SELECT * FROM ydeployments;
┌───────────┬─────────────────┬────────────────────┐
│ Typecode │ TableName │ ExtensionName │
├───────────┼─────────────────┼────────────────────┤
│ 1 │ items │ core │
│ 23 │ products │ catalog │
│ 25000 │ loyalty_tx │ myextension │
└───────────┴─────────────────┴────────────────────┘
Localized Attributes
Localization is a first-class citizen in the type system. Any attribute can be made localizable.
Defining Localized Attributes
<attribute qualifier="name" type="localized:java.lang.String">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="description" type="localized:java.lang.String">
<modifiers optional="true"/>
<persistence type="property">
<columntype>
<value>HYBRIS.LONG_STRING</value>
</columntype>
</persistence>
</attribute>
Database Storage
Localized attributes are stored in a separate *lp table (localized properties):
products table: productslp table:
┌────────┬──────────┐ ┌───────────┬──────────┬──────────────┬───────────────┐
│ PK │ p_code │ │ ITEMPK │ LANGPK │ p_name │ p_description │
├────────┼──────────┤ ├───────────┼──────────┼──────────────┼───────────────┤
│ 123 │ PROD001 │ │ 123 │ en │ Blue Shirt │ A nice shirt │
│ │ │ │ 123 │ de │ Blaues Hemd │ Ein Hemd │
│ │ │ │ 123 │ fr │ Chemise Bleu│ Une chemise │
└────────┴──────────┘ └───────────┴──────────┴──────────────┴───────────────┘
Using Localized Attributes in Code
// Get in current session language
String name = product.getName();
// Get in specific locale
String germanName = product.getName(Locale.GERMAN);
// Set in current session language
product.setName("Blue Shirt");
// Set in specific locale
product.setName("Blaues Hemd", Locale.GERMAN);
// Get all localizations as a Map
Map<Locale, String> allNames = product.getAllName();
Querying Localized Attributes
-- Query in a specific language
SELECT {pk}, {name[en]} FROM {Product} WHERE {name[de]} LIKE '%Hemd%'
-- Query in the current session language (no language qualifier)
SELECT {pk}, {name} FROM {Product} WHERE {name} LIKE '%Shirt%'
Type System and FlexibleSearch
FlexibleSearch is the query language for the type system. Understanding how type system concepts map to queries is essential.
Curly Brace Syntax Resolution
When you write:
SELECT {pk}, {code}, {name} FROM {Product}
The platform resolves:
-
{Product}→ looks up theComposedTypefor "Product", finds its deployment table (products) -
{pk}→ maps to thePKcolumn -
{code}→ finds theAttributeDescriptorfor "code", maps top_codecolumn -
{name}→ finds it's localized, joins theproductslptable filtered by session language
The generated SQL (simplified) is:
SELECT item.PK, item.p_code, lp.p_name
FROM products item
LEFT JOIN productslp lp ON item.PK = lp.ITEMPK AND lp.LANGPK = ?sessionLanguagePK
Polymorphic Queries
Because the type system tracks inheritance, FlexibleSearch supports polymorphic queries:
-- Returns Products AND all subtypes (like ApparelProduct, ElectronicsProduct)
SELECT {pk} FROM {Product}
-- Returns ONLY exact Product instances (no subtypes)
SELECT {pk} FROM {Product!}
-- The ! suffix restricts to the exact type
Type-Aware Joins
-- Join using relation definitions
SELECT {o.pk}, {o.code}, {c.uid}
FROM {Order AS o JOIN Customer AS c ON {o.customer} = {c.pk}}
WHERE {c.loyaltyTier} = ?tier
-- The platform resolves {o.customer} to the FK column automatically
Querying Enums
-- Enum values are referenced by their enumeration code
SELECT {pk} FROM {Customer} WHERE {loyaltyTier} =
({{SELECT {pk} FROM {EnumerationValue} WHERE {code} = 'GOLD'
AND {Type} = ({{SELECT {pk} FROM {ComposedType} WHERE {code} = 'LoyaltyTier'}})}})
-- Or using the simpler HybrisEnumValue approach with parameters
FlexibleSearchQuery query = new FlexibleSearchQuery(
"SELECT {pk} FROM {Customer} WHERE {loyaltyTier} = ?tier");
query.addQueryParameter("tier", LoyaltyTier.valueOf("GOLD"));
Indexes
Indexes in items.xml map directly to database indexes:
<indexes>
<!-- Simple index -->
<index name="codeIdx">
<key attribute="code"/>
</index>
<!-- Unique index (also creates a unique constraint) -->
<index name="uniqueCodeIdx" unique="true">
<key attribute="code"/>
</index>
<!-- Composite index -->
<index name="customerDateIdx">
<key attribute="customer"/>
<key attribute="transactionDate"/>
</index>
</indexes>
Generated DDL:
CREATE INDEX codeIdx ON loyalty_tx (p_code);
CREATE UNIQUE INDEX uniqueCodeIdx ON loyalty_tx (p_code);
CREATE INDEX customerDateIdx ON loyalty_tx (p_customer, p_txdate);
Index Best Practices
- Index attributes used in WHERE clauses of your FlexibleSearch queries
- Composite indexes should follow the leftmost prefix rule — put the most selective column first
- Don't over-index — each index slows down writes
-
Unique indexes on
codeoruidattributes are almost always a good idea - Monitor slow queries in HAC and add indexes as needed
Best Practices
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Type codes | PascalCase, descriptive |
LoyaltyTransaction, CustomProduct
|
| Attribute qualifiers | camelCase |
loyaltyPoints, transactionDate
|
| Relation codes | Source2TargetRelation |
Customer2LoyaltyTransactionRelation |
| Enum codes | PascalCase |
LoyaltyTier, TransactionType
|
| Table names | lowercase, underscore |
loyalty_tx, custom_products
|
| Index names | camelCase + Idx suffix |
codeIdx, customerDateIdx
|
Typecode Management
- Maintain a typecode registry document in your project
- Assign ranges per extension (e.g.,
mycore: 25000–25099,myfacades: 25100–25199) - Never reuse a typecode that was previously assigned to a different type
- In multi-team projects, agree on typecode ranges to avoid collisions
When to Create New Types vs. Extend Existing
Create a new type when:
- The entity represents a new domain concept (e.g.,
LoyaltyTransaction) - You need dedicated table storage for performance
- The entity has its own lifecycle (CRUD operations)
Extend an existing type when:
- You're adding attributes to OOTB types (e.g., adding
loyaltyTiertoCustomer) - The additional data logically belongs to the existing type
- You don't need a separate table
Performance Considerations
- Keep type tables focused: Don't add 50 attributes to a single type — consider splitting into related types
-
Use dedicated deployments: Types without deployments use single-table inheritance or the
propstable — both have performance costs at scale - Watch collection-type attributes: They're serialized into a single column — terrible for large collections. Use relations instead
-
Localized attributes add JOIN overhead: Every localized attribute requires a JOIN to the
*lptable. Only localize what truly needs multiple languages - Dynamic attributes are computed per access: Cache-intensive code shouldn't call dynamic attribute getters in tight loops
Real-World Modeling Patterns
Product Hierarchy with Variants
<!-- Base product type (often already exists in OOTB) -->
<itemtype code="ApparelProduct" extends="Product" autocreate="true" generate="true"
jaloclass="com.mycompany.jalo.ApparelProduct">
<deployment table="apparel_products" typecode="25010"/>
<attributes>
<attribute qualifier="genderList" type="GenderList">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
<!-- Style variant (color) -->
<itemtype code="ApparelStyleVariantProduct" extends="VariantProduct" autocreate="true" generate="true"
jaloclass="com.mycompany.jalo.ApparelStyleVariantProduct">
<deployment table="apparel_style_variants" typecode="25011"/>
<attributes>
<attribute qualifier="color" type="localized:java.lang.String">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="swatchImage" type="Media">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
<!-- Size variant -->
<itemtype code="ApparelSizeVariantProduct" extends="ApparelStyleVariantProduct" autocreate="true" generate="true"
jaloclass="com.mycompany.jalo.ApparelSizeVariantProduct">
<deployment table="apparel_size_variants" typecode="25012"/>
<attributes>
<attribute qualifier="size" type="localized:java.lang.String">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
Customer Extension for B2B
<itemtype code="B2BCustomer" autocreate="false" generate="false">
<attributes>
<attribute qualifier="creditLimit" type="java.math.BigDecimal">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="approvalThreshold" type="java.math.BigDecimal">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="requiresApproval" type="java.lang.Boolean">
<defaultvalue>Boolean.TRUE</defaultvalue>
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
Custom Order Attributes
<itemtype code="Order" autocreate="false" generate="false">
<attributes>
<attribute qualifier="giftMessage" type="localized:java.lang.String">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="preferredDeliveryDate" type="java.util.Date">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="loyaltyPointsEarned" type="java.lang.Integer">
<defaultvalue>Integer.valueOf(0)</defaultvalue>
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
Top comments (0)