If you're a Java developer stepping into the SAP Commerce world — or a mid-level Commerce developer who wants to truly understand what's happening under the hood — this article is for you. We'll go beyond surface-level overviews and dig into the internals: the class hierarchies, the extension loading mechanism, the type system's relationship to the database, and the design patterns that make the platform tick.
High-Level Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ Composable Storefront (Spartacus) │ Mobile │ Custom SPA │
└──────────────────────┬──────────────────────────────────────────┘
│ HTTPS (REST/JSON)
┌──────────────────────▼──────────────────────────────────────────┐
│ OCC REST API LAYER │
│ @Controller classes in ycommercewebservices / custom OCC ext │
│ WsDTO objects │ Orika Mappers │ OAuth2 (spring-security) │
└──────────────────────┬──────────────────────────────────────────┘
│ Java method calls
┌──────────────────────▼──────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Facades │ │ Services │ │ Strategies / Hooks │ │
│ │ (DTOs out) │→ │ (Business │→ │ (FindPriceStrategy, │ │
│ │ │ │ Logic) │ │ AddToCartStrategy...) │ │
│ └─────────────┘ └──────┬───────┘ └────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────────────┐ │
│ │ DAOs (FlexibleSearchQuery → GenericSearchService) │ │
│ └───────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────────────┐ │
│ │ Model Objects (generated from items.xml) │ │
│ └───────────────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────────────┘
│ JDBC
┌──────────────────────▼──────────────────────────────────────────┐
│ PERSISTENCE LAYER │
│ RDBMS (SAP HANA / MySQL / HSQLDB / Oracle / PostgreSQL) │
│ + Solr (product search) + Media Storage (local / S3 / Azure) │
└─────────────────────────────────────────────────────────────────┘
Client Layer
The client layer is anything that consumes Commerce APIs. In modern projects, this is typically Composable Storefront (Spartacus) — an Angular SPA that communicates exclusively via OCC REST APIs. Legacy projects may still use the JSP-based Accelerator storefront, which runs inside the Commerce JVM itself.
OCC REST API Layer
The OCC (Omni Commerce Connect) layer exposes RESTful endpoints under /occ/v2/. The entry point classes live in the ycommercewebservices extension (or your custom OCC extension). Key internals:
-
Controllers extend Spring's
@Controllerand use@RequestMappingannotations - WsDTO classes (Data Transfer Objects) are the API contract — generated or hand-crafted POJOs annotated for serialization
-
Orika Mappers (
Converter<Source, Target>) translate between internal Models/Data objects and WsDTOs -
OAuth2 authentication is handled via Spring Security OAuth, configured in
ycommercewebservicesSpring context
Application Layer
This is where business logic lives. The critical class hierarchy:
-
Facades (e.g.,
DefaultProductFacade,DefaultCartFacade) — orchestrate multiple services and convert Models into Data objects (DTOs). Facades implement interfaces likeProductFacade,CartFacade. -
Services (e.g.,
DefaultProductService,DefaultCartService) — contain business logic. They extendAbstractBusinessServiceor implement interfaces likeProductService,CartService. They operate on Model objects. -
DAOs (e.g.,
DefaultProductDao) — executeFlexibleSearchQueryobjects viaFlexibleSearchServiceand return Model objects. -
Strategies — pluggable business rules. For example,
FindPriceStrategydetermines product pricing,CommerceAddToCartStrategyhandles add-to-cart logic.
Platform Layer
Under all the commerce-specific code lies the platform itself. Key platform classes:
-
Registry— the bootstrap class.Registry.activateStandaloneMode()orRegistry.activateMasterTenant()starts the platform. -
Tenant— represents an isolated runtime context.MasterTenantis the primary tenant. -
TypeManager— manages the runtime type system, loaded fromitems.xmldefinitions across all extensions. -
ModelService— the persistence gateway.modelService.save(model),modelService.remove(model),modelService.create(MyModel.class).
Persistence Layer
SAP Commerce supports multiple databases through JDBC. In CCv2 (Cloud), SAP HANA is the standard database. The platform generates DDL from the type system definitions — you never write CREATE TABLE statements manually.
Solr (via the solrfacetsearch extension) provides full-text product search with faceting. ZooKeeper manages the Solr cluster in CCv2.
The Type System
The type system is what makes SAP Commerce fundamentally different from a standard JPA/Hibernate application. Instead of writing @Entity classes and Flyway migrations, you define your data model in items.xml files, and the platform generates both Java Model classes and database DDL.
items.xml Deep Dive
Here's a realistic items.xml example:
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<enumtypes>
<enumtype code="LoyaltyTier" autocreate="true" generate="true" dynamic="true">
<value code="BRONZE"/>
<value code="SILVER"/>
<value code="GOLD"/>
<value code="PLATINUM"/>
</enumtype>
</enumtypes>
<itemtypes>
<itemtype code="Customer" autocreate="false" generate="false">
<!-- Extending the existing Customer type -->
<attributes>
<attribute qualifier="loyaltyTier" type="LoyaltyTier">
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="loyaltyPoints" type="java.lang.Integer">
<defaultvalue>Integer.valueOf(0)</defaultvalue>
<modifiers optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="loyaltyScore" type="java.lang.Double">
<modifiers read="true" write="false" optional="true"/>
<persistence type="dynamic" attributeHandler="customerLoyaltyScoreHandler"/>
</attribute>
</attributes>
</itemtype>
<itemtype code="LoyaltyTransaction" autocreate="true" generate="true"
extends="GenericItem"
jaloclass="com.mycompany.jalo.LoyaltyTransaction">
<deployment table="loyalty_transactions" typecode="25000"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<modifiers unique="true" optional="false"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="points" type="java.lang.Integer">
<modifiers optional="false"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="transactionDate" type="java.util.Date">
<modifiers optional="false"/>
<persistence type="property"/>
</attribute>
</attributes>
<indexes>
<index name="codeIdx" unique="true">
<key attribute="code"/>
</index>
<index name="dateIdx">
<key attribute="transactionDate"/>
</index>
</indexes>
</itemtype>
</itemtypes>
<relations>
<relation code="Customer2LoyaltyTransaction" localized="false">
<sourceElement type="Customer" qualifier="customer" cardinality="one">
<modifiers optional="false"/>
</sourceElement>
<targetElement type="LoyaltyTransaction" qualifier="loyaltyTransactions" cardinality="many"
collectiontype="list" ordered="true">
<modifiers partof="true"/>
</targetElement>
</relation>
</relations>
</items>
Key concepts:
-
autocreate="false" generate="false"onCustomer: We're extending an existing platform type, not creating a new one. The platform merges attributes from all extensions'items.xmlfiles for the same type code. -
deployment: Maps the type to a specific database table and assigns a unique typecode (integer). Typecodes must be unique across the system (custom types should use 10000+). -
persistence type="dynamic": The attribute isn't stored in the database. Instead, aDynamicAttributeHandlerSpring bean computes the value at runtime. -
persistence type="property": Standard database column storage. -
partof="true"on the relation target: Defines a composition relationship — deleting the Customer will cascade-delete their LoyaltyTransactions.
How Types Map to the Database
When you run ant initialize or ant updatesystem, the platform's SchemaGenerator reads the merged type system and generates DDL:
- Each
itemtypewith adeploymentgets its own table - Types without a
deploymentstore their attributes in the parent type's table (single-table inheritance) - Localized attributes get a separate
*lptable (e.g.,products→productslpfor localized product names) - Many-to-many relations create link tables
- The
propstable stores attributes that overflow a type's dedicated table (configurable viaimpex.legacy.mode)
The internal class TypeManagerImpl builds the runtime ComposedType graph. You can inspect it at runtime:
TypeManager typeManager = TypeManager.getInstance();
ComposedType customerType = typeManager.getComposedType("Customer");
Set<AttributeDescriptor> attributes = customerType.getAttributeDescriptors();
Generated Model Classes
After modifying items.xml and running ant build, the platform generates:
-
GeneratedLoyaltyTransactionModel.javaingensrc/— generated getters/setters, do not edit - You create
LoyaltyTransactionModel.javainsrc/extending the generated class — add custom logic here
The generated model class extends ItemModel and uses the ModelService internally for persistence:
// Generated
public class GeneratedLoyaltyTransactionModel extends ItemModel {
public static final String CODE = "code";
public static final String POINTS = "points";
public String getCode() {
return getPersistenceContext().getPropertyValue(CODE);
}
public void setCode(String value) {
getPersistenceContext().setPropertyValue(CODE, value);
}
// ...
}
The PersistenceContext handles dirty tracking and lazy loading transparently.
Type System vs. Traditional ORM
| Aspect | JPA/Hibernate | SAP Commerce Type System |
|---|---|---|
| Model definition |
@Entity Java annotations |
items.xml XML |
| Schema migration | Flyway/Liquibase | ant updatesystem |
| Model generation | N/A (you write entities) | Platform generates models from XML |
| Relationships |
@OneToMany, @ManyToMany
|
<relation> elements in XML |
| Localization | Manual (separate tables or JSON) | Built-in via localized: prefix |
| Dynamic attributes | N/A | DynamicAttributeHandler |
| Query language | JPQL/HQL | FlexibleSearch |
| Inheritance |
@Inheritance strategies |
Type hierarchy in XML (extends=) |
Configuration Hierarchy
SAP Commerce has a sophisticated configuration system with a clear precedence order. Understanding this is critical for debugging "why is my property not taking effect?"
Priority (highest wins):
─────────────────────────────────────────────
1. System properties (-Dproperty=value)
2. Environment variables (via y_ prefix convention in CCv2)
3. local.properties (project-level overrides)
4. Extension properties (myextension/project.properties)
5. Platform defaults (platform/project.properties)
─────────────────────────────────────────────
In Practice
# platform/project.properties (platform defaults)
db.pool.maxActive=30
# myextension/project.properties (extension defaults)
my.feature.enabled=false
# config/local.properties (project overrides, NOT in source control for local dev)
db.pool.maxActive=50
my.feature.enabled=true
db.url=jdbc:mysql://localhost:3306/hybris
# In CCv2 manifest.json (environment-specific)
{
"properties": [
{ "key": "db.pool.maxActive", "value": "100", "persona": "production" },
{ "key": "my.feature.enabled", "value": "true" }
]
}
Access configuration in code via:
Config.getParameter("my.feature.enabled"); // returns String
Config.getBoolean("my.feature.enabled", false); // with default
Config.getInt("db.pool.maxActive", 30);
The Config class reads from ConfigIntf, which loads all property sources at startup and merges them according to the precedence rules. In CCv2, environment-specific properties from manifest.json are injected as system properties or written to local.properties during the build.
Key Design Patterns
Populator / Converter Pattern
This is the most pervasive pattern in SAP Commerce. It separates the concern of converting Models to DTOs into small, composable units.
// Converter interface (from platform)
public interface Converter<SOURCE, TARGET> extends Populator<SOURCE, TARGET> {
TARGET convert(SOURCE source);
TARGET convert(SOURCE source, TARGET prototype);
}
// Populator interface
public interface Populator<SOURCE, TARGET> {
void populate(SOURCE source, TARGET target);
}
A typical converter delegates to a list of populators:
// Spring configuration
<bean id="loyaltyTransactionConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.mycompany.data.LoyaltyTransactionData"/>
<property name="populators">
<list>
<ref bean="loyaltyTransactionBasicPopulator"/>
<ref bean="loyaltyTransactionPointsPopulator"/>
</list>
</property>
</bean>
<bean id="loyaltyTransactionBasicPopulator"
class="com.mycompany.facades.populators.LoyaltyTransactionBasicPopulator"/>
public class LoyaltyTransactionBasicPopulator
implements Populator<LoyaltyTransactionModel, LoyaltyTransactionData> {
@Override
public void populate(LoyaltyTransactionModel source, LoyaltyTransactionData target) {
target.setCode(source.getCode());
target.setTransactionDate(source.getTransactionDate());
}
}
Why this pattern matters: You can add new populators via Spring without modifying existing code. An extension can add a loyaltyTransactionCustomPopulator to the converter's populator list using Spring <list merge="true">, extending the DTO conversion without touching the original code. This is critical for upgradeability.
Strategy Pattern
Many business decisions in SAP Commerce are implemented as strategies — pluggable beans that can be swapped via Spring configuration.
Key strategies in the platform:
| Strategy Interface | Purpose | Default Implementation |
|---|---|---|
CommercePlaceOrderStrategy |
Place order logic | DefaultCommercePlaceOrderStrategy |
CommerceAddToCartStrategy |
Add to cart validation & logic | DefaultCommerceAddToCartStrategy |
FindPriceStrategy |
Price resolution | Europe1FindPriceStrategy |
FindDiscountValuesStrategy |
Discount resolution | Europe1FindDiscountValuesStrategy |
DeliveryModeLookupStrategy |
Available delivery modes | DefaultDeliveryModeLookupStrategy |
CommerceStockLevelCalculationStrategy |
Stock level calculation | DefaultCommerceStockLevelCalculationStrategy |
To customize pricing logic, you'd implement FindPriceStrategy and register it in Spring:
<bean id="findPriceStrategy" class="com.mycompany.strategies.CustomFindPriceStrategy"/>
The platform resolves strategies by bean ID convention or via explicit injection.
Interceptor Chain
Interceptors are lifecycle hooks on ModelService operations. When you call modelService.save(model), the platform executes a chain of interceptors before and after persistence.
The interceptor types, in execution order:
-
InitDefaultsInterceptor— Called duringmodelService.create(). Sets default values. -
PrepareInterceptor— Called before save. Transforms data (e.g., generate a code, compute a derived field). -
ValidateInterceptor— Called before save, after prepare. Validates business rules. ThrowsInterceptorExceptionto abort. -
RemoveInterceptor— Called beforemodelService.remove(). Can prevent deletion or clean up related data. -
LoadInterceptor— Called when a model is loaded from the database. Rarely used.
public class LoyaltyTransactionPrepareInterceptor implements PrepareInterceptor<LoyaltyTransactionModel> {
@Override
public void onPrepare(LoyaltyTransactionModel model, InterceptorContext ctx) throws InterceptorException {
if (model.getCode() == null) {
model.setCode("LT-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
}
}
}
Registration in Spring:
<bean id="loyaltyTransactionPrepareInterceptor"
class="com.mycompany.interceptors.LoyaltyTransactionPrepareInterceptor"/>
<bean id="loyaltyTransactionPrepareInterceptorMapping"
class="de.hybris.platform.servicelayer.interceptor.impl.InterceptorMapping">
<property name="interceptor" ref="loyaltyTransactionPrepareInterceptor"/>
<property name="typeCode" value="LoyaltyTransaction"/>
</bean>
Important internal detail: The InterceptorRegistry class maintains mappings from type codes to interceptor lists. The DefaultModelService iterates through InterceptorExecutionPolicy to execute the chain. You can disable interceptors for bulk operations:
Map<String, Object> context = new HashMap<>();
context.put(InterceptorExecutionPolicy.DISABLED_INTERCEPTOR_BEANS,
Set.of("loyaltyTransactionPrepareInterceptor"));
modelService.save(model, context);
Event System
SAP Commerce has an internal event system based on AbstractEvent and AbstractEventListener:
// Custom event
public class LoyaltyPointsEarnedEvent extends AbstractEvent {
private final String customerUid;
private final int points;
public LoyaltyPointsEarnedEvent(String customerUid, int points) {
this.customerUid = customerUid;
this.points = points;
}
// getters...
}
// Listener
public class LoyaltyPointsEarnedListener extends AbstractEventListener<LoyaltyPointsEarnedEvent> {
@Override
protected void onEvent(LoyaltyPointsEarnedEvent event) {
// Send notification, update analytics, etc.
}
}
// Publishing
eventService.publishEvent(new LoyaltyPointsEarnedEvent("john.doe", 100));
Events can be cluster-aware (broadcast to all nodes) by extending ClusterAwareEvent and implementing publish(int sourceNodeId).
When to Customize vs. Extend vs. Use OOTB
| Approach | When to Use | Risk Level |
|---|---|---|
| OOTB (Out of the Box) | Feature matches requirements 80%+ | Low |
| Configuration | Behavior can be adjusted via properties/ImpEx | Low |
| Extend (Add Populator/Strategy/Interceptor) | Need additional behavior without changing existing code | Medium |
| Override (Replace Bean) | Need to change existing behavior | Medium-High |
| Custom Extension | New domain/feature not covered by OOTB | High |
| Modify OOTB Code | NEVER — breaks upgrades | Critical |
Golden Rule: Never modify files in bin/platform/ or bin/modules/. Always customize through your own extensions using Spring bean overriding, type system extension, and the patterns described above.
Top comments (0)